8Regression tests for :mod:`oct.mcp.safety` — ``SafetyGate`` HITL
13- Verify ``SafetyGate.check()`` HITL rules (rules 1-5).
14- Verify oct_format double guard (apply + confirm).
15- Verify non-destructive tools always proceed; destructive tools
17- Verify ``_format_confirmation_request`` sentinel and content.
18- Verify T-08: no stateful replay surface (each call independent).
25 L3 — assertion details
30- Tests are in-memory safety checks; no subprocess or filesystem writes.
33from __future__
import annotations
43 _format_confirmation_request,
44 _TOOL_RISK_DESCRIPTIONS,
63 return ToolSpec(cli=
"lint", permissions=frozenset({
"read"}), destructive=
False, auto_approve=
True)
67 return TOOL_MANIFEST[tool]
77 d = gate.check(
"oct_lint", spec, {})
78 assert d.proceed
is True
79 assert d.requires_confirmation
is False
80 assert d.dry_run
is False
84 d = gate.check(
"oct_lint", spec, {
"confirm":
False})
85 assert d.proceed
is True
89 d = gate.check(
"oct_health", spec, {
"confirm":
True})
90 assert d.proceed
is True
94 "oct_lint",
"oct_health",
"oct_skeleton",
"oct_deps",
95 "oct_test",
"oct_typecheck",
"oct_diag",
"oct_git_status",
"oct_git_check",
97 for tool_name
in phase5a:
98 spec = TOOL_MANIFEST[tool_name]
99 d = gate.check(tool_name, spec, {})
100 assert d.proceed
is True, f
"{tool_name} should proceed"
101 assert d.dry_run
is False
109 @pytest.mark.parametrize(
"tool_name", [
110 "oct_format",
"oct_docs",
"oct_export_source",
"oct_new",
111 "oct_scaffold",
"oct_clean",
"oct_install_hooks",
112 "oct_git_commit",
"oct_git_hooks_install",
"oct_git_init",
116 spec = TOOL_MANIFEST[tool_name]
117 args = {
"confirm":
False}
119 if tool_name ==
"oct_new":
120 args[
"path"] =
"src/foo.py"
121 if tool_name ==
"oct_scaffold":
122 args[
"name"] =
"myproject"
123 if tool_name ==
"oct_git_commit":
124 args[
"message"] =
"feat: add feature"
125 d = gate.check(tool_name, spec, args)
126 assert d.proceed
is False, f
"{tool_name} should not proceed without confirm"
127 assert d.requires_confirmation
is True
128 assert d.dry_run
is False
131 spec = TOOL_MANIFEST[
"oct_clean"]
132 d = gate.check(
"oct_clean", spec, {})
133 assert d.proceed
is False
134 assert d.requires_confirmation
is True
137 spec = TOOL_MANIFEST[
"oct_git_init"]
138 d = gate.check(
"oct_git_init", spec, {
"confirm":
False})
139 assert d.proceed
is False
140 assert d.requires_confirmation
is True
148 @pytest.mark.parametrize(
"tool_name", [
149 "oct_docs",
"oct_export_source",
"oct_new",
150 "oct_scaffold",
"oct_clean",
"oct_install_hooks",
151 "oct_git_commit",
"oct_git_hooks_install",
"oct_git_init",
155 spec = TOOL_MANIFEST[tool_name]
156 args: dict = {
"confirm":
True}
157 if tool_name ==
"oct_new":
158 args[
"path"] =
"src/foo.py"
159 if tool_name ==
"oct_scaffold":
160 args[
"name"] =
"myproject"
161 if tool_name ==
"oct_git_commit":
162 args[
"message"] =
"feat: add feature"
163 d = gate.check(tool_name, spec, args)
164 assert d.proceed
is True, f
"{tool_name} should proceed with confirm=True"
165 assert d.requires_confirmation
is False
166 assert d.dry_run
is False
175 spec = TOOL_MANIFEST[
"oct_format"]
176 d = gate.check(
"oct_format", spec, {
"confirm":
False,
"apply":
False})
177 assert d.proceed
is False
178 assert d.requires_confirmation
is True
181 spec = TOOL_MANIFEST[
"oct_format"]
182 d = gate.check(
"oct_format", spec, {
"confirm":
True,
"apply":
False})
183 assert d.proceed
is True
184 assert d.dry_run
is True
185 assert d.requires_confirmation
is False
188 spec = TOOL_MANIFEST[
"oct_format"]
189 d = gate.check(
"oct_format", spec, {
"confirm":
True,
"apply":
True})
190 assert d.proceed
is True
191 assert d.dry_run
is False
192 assert d.requires_confirmation
is False
195 spec = TOOL_MANIFEST[
"oct_format"]
197 d = gate.check(
"oct_format", spec, {
"confirm":
True})
198 assert d.proceed
is True
199 assert d.dry_run
is True
208 spec = TOOL_MANIFEST[
"oct_format"]
209 result = _format_confirmation_request(
"oct_format", spec, {
"apply":
False,
"confirm":
False})
210 assert result.startswith(HITL_SENTINEL)
213 spec = TOOL_MANIFEST[
"oct_clean"]
214 result = _format_confirmation_request(
"oct_clean", spec, {
"dry_run":
True})
215 assert "oct_clean" in result
218 spec = TOOL_MANIFEST[
"oct_scaffold"]
219 result = _format_confirmation_request(
"oct_scaffold", spec, {
"name":
"myproject"})
220 assert "confirm=True" in result
223 spec = TOOL_MANIFEST[
"oct_git_commit"]
224 result = _format_confirmation_request(
225 "oct_git_commit", spec, {
"message":
"feat: test"}
227 assert "commit" in result.lower()
230 spec = TOOL_MANIFEST[
"oct_format"]
231 result = _format_confirmation_request(
232 "oct_format", spec, {
"confirm":
False,
"apply":
False,
"paths": []}
237 assert result.startswith(HITL_SENTINEL)
240 for tool_name
in _TOOL_RISK_DESCRIPTIONS:
241 assert _TOOL_RISK_DESCRIPTIONS[tool_name], f
"{tool_name} risk desc should not be empty"
244 spec =
ToolSpec(cli=
"unknown", permissions=frozenset({
"write"}), destructive=
True)
245 result = _format_confirmation_request(
"oct_mystery", spec, {})
246 assert result.startswith(HITL_SENTINEL)
247 assert "oct_mystery" in result
256 d =
SafetyDecision(proceed=
True, requires_confirmation=
False, dry_run=
False, reason=
"ok")
257 assert d.proceed
is True
258 assert d.requires_confirmation
is False
259 assert d.dry_run
is False
260 assert d.reason ==
"ok"
263 d =
SafetyDecision(proceed=
False, requires_confirmation=
True, dry_run=
False, reason=
"need confirm")
264 assert d.proceed
is False
265 assert d.requires_confirmation
is True
274 """T-08: successive calls with same args produce same result (no state)."""
275 spec = TOOL_MANIFEST[
"oct_clean"]
276 args = {
"confirm":
True,
"dry_run":
True}
277 d1 = gate.check(
"oct_clean", spec, args)
278 d2 = gate.check(
"oct_clean", spec, args)
279 assert d1.proceed == d2.proceed
280 assert d1.requires_confirmation == d2.requires_confirmation
283 """Calling with confirm=False then confirm=True works correctly (no stale state)."""
284 spec = TOOL_MANIFEST[
"oct_git_init"]
285 d1 = gate.check(
"oct_git_init", spec, {
"confirm":
False})
286 assert d1.proceed
is False
287 d2 = gate.check(
"oct_git_init", spec, {
"confirm":
True})
288 assert d2.proceed
is True
291 """Two SafetyGate instances with the same config are both stateless."""
295 spec = TOOL_MANIFEST[
"oct_scaffold"]
296 args = {
"confirm":
True,
"name":
"myproject"}
297 d1 = gate1.check(
"oct_scaffold", spec, args)
298 d2 = gate2.check(
"oct_scaffold", spec, args)
299 assert d1.proceed == d2.proceed
test_write_tools_proceed_with_confirm(self, gate, tool_name)
test_confirm_false_is_default(self, gate)
test_all_write_tools_require_confirmation(self, gate, tool_name)
test_confirm_false_explicit(self, gate)
test_each_call_independent(self, gate)
test_multiple_gate_instances_independent(self)
test_first_false_second_true_independent(self, gate)
test_fields_present(self)
_write_spec(str tool="oct_format")