Files

69 lines
4.3 KiB
Python
Raw Permalink Normal View History

import hashlib
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from console.settings import Settings
from console.store import Store
from console.native_plans import build_plan
from console.native_rpc import handle
class NativeRPCTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
source = root / "source"
source.mkdir()
entry = source / "test.py"
entry.write_text("print(1)\n")
self.settings = Settings(root / "runtime", [{"id": "original", "path": str(source)}])
self.settings.test_definitions = [{"key": "native", "source_group": "original", "entry": "test.py", "name": "原环境样例"}]
self.settings.native_catalog = {"targets": [], "profiles": [{"id": "native", "source_group": "original", "entry": "test.py",
"cwd": str(source), "user": "root", "interpreter": "/usr/bin/python3", "parameters": [], "arguments": [],
"impact": {"level": "service", "title": "服务切换", "details": ["会停止指定服务"], "requires_confirmation": True}}]}
self.script = {"id": "a" * 32, "name": "test.py", "description": "", "kind": "external", "source_group": "original",
"source_path": str(entry), "sha256": hashlib.sha256(entry.read_bytes()).hexdigest(), "language": "python", "version": 1,
"enabled": True, "status": "ready", "config": {"native_options": {}, "timeout_seconds": 300}}
self.store = Store(self.settings)
self.store.save_script(self.script)
self.plan = build_plan(self.settings, self.script, check_host=False)
self.request = {"op": "queue", "script_id": self.script["id"], "request_id": "5b0202b7-851d-48ad-a7b9-dce22f000001",
"plan_token": self.plan["plan_token"], "source_ip": "127.0.0.1"}
def tearDown(self):
self.temp.cleanup()
def test_server_requires_explicit_current_confirmation(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
with self.assertRaises(ValueError):handle(self.settings, self.store, self.request)
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True, "plan_token": "old"})
self.assertEqual(self.store.runs(), [])
run = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
self.assertTrue(run["execution"]["impact_acknowledged"])
self.assertEqual(run["execution"]["mode"], "native")
def test_retry_returns_original_even_if_plan_later_changed(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
first = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with patch("console.native_rpc.native_plans.build_plan", side_effect=ValueError("changed")):
again = handle(self.settings, self.store, self.request)
self.assertEqual(first["id"], again["id"])
self.assertEqual(len(self.store.runs()), 1)
def test_failed_preflight_and_unknown_operation_never_queue(self):
with patch("console.native_rpc.native_plans.build_plan", return_value={**self.plan, "ready": False}):
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "exec", "command": "id"})
self.assertEqual(self.store.runs(), [])
def test_input_rejected_for_queued_or_noninteractive_run(self):
run = self.store.queue(self.script, "sample", "127.0.0.1", execution={"mode": "native", "interactive": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": run["id"], "text": "1"})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": "../../bad", "text": "1"})
def test_input_channel_rejects_tmux_keys_and_shell_commands(self):
for text in ("\x02c", "\x1b", "bash", "1\nid", "echo test", ":new-window"):
with self.assertRaisesRegex(ValueError, "控制键"):
handle(self.settings, self.store, {"op": "input", "run_id": "a"*32, "text": text})