Files
Agent-System-Dev/tests/test_svc_user_binding.py
T
weichenandCursor 2a03dd58f9 Bind svc user CLI to instance storage and reject lock bypass
Why: U1 requires account ops to hit manifest data_root/DSN with OpenExisting and refuse missing flock or default ./data.
Evidence: python3 -m unittest tests.test_svc_user_binding (T1–T5) + test_svc_identity
Impact: svc <env> user … calls ENV_BIN with --open-existing; deploy/user fail closed without flock.
Refs: docs/svc-deploy/u1-instance-account-binding.md; Backend OpenExisting (PR #171)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 03:55:24 +00:00

172 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""svc.sh 账户 CLI 实例绑定(U1 规格 §6 T1–T5)。
覆盖:合法 manifest 下 user list 命中 data_root+open-existing(T1)、禁止覆盖
--data(T2)、删除 data 根后 OpenExisting 拒绝且不建空库(T3)、无 flock 拒绝
user/deploy(T4)。T5 身份回归由 test_svc_identity 同发现执行;此处仅冒烟。
桩与夹具复用 tests/test_svc_recover.py;manifest 辅助对齐 test_svc_identity。
"""
import importlib.util
import json
import os
import shutil
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
tsr = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(tsr)
class UserBindingTests(tsr.SvcAutoRecoverTests):
def setUp(self):
super().setUp()
shutil.copy2(SOURCE / "scripts" / "svc_manifest.py",
self.root / "scripts" / "svc_manifest.py")
self.trace.mkdir(exist_ok=True)
def manifest(self, name):
p = self.runtime(name) / "manifest.json"
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else None
def running(self, name):
pid = self.pid_of(name)
if not pid:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False
def touch_db(self, name):
data = Path(self.manifest(name)["identity"]["data_root"])
data.mkdir(parents=True, exist_ok=True)
(data / "agent-system.db").write_bytes(b"sqlite-stub")
def cli_trace(self):
f = self.trace / "cli.txt"
return f.read_text(encoding="utf-8") if f.exists() else ""
def clear_cli_trace(self):
f = self.trace / "cli.txt"
if f.exists():
f.unlink()
# ---- T1 合法 manifest:user list 绑定 data_root + --open-existing ----
def test_T1_user_list_binds_manifest_data(self):
self.seed_agent("ub1", 9441)
self.start_ok("ub1")
self.svc("ub1", "stop")
root = self.manifest("ub1")["identity"]["data_root"]
self.touch_db("ub1")
self.clear_cli_trace()
result = self.svc("ub1", "user", "list")
self.assertIn("op", result.stdout)
trace = self.cli_trace()
self.assertIn("user list", trace)
self.assertIn("--open-existing", trace)
self.assertIn("--data %s" % root, trace)
self.assertNotIn("./data", trace)
# ---- T2 禁止覆盖 --data;未绑定默认目录拒绝 ----
def test_T2_reject_override_data_flag(self):
self.seed_agent("ub2", 9442)
self.start_ok("ub2")
self.svc("ub2", "stop")
self.touch_db("ub2")
other = self.root / "evil-data"
other.mkdir()
(other / "agent-system.db").write_bytes(b"x")
result = self.svc("ub2", "user", "list", "--data", str(other), check=False)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(any(k in result.stdout for k in ("禁止覆盖", "勿传")), result.stdout)
self.assertFalse((self.trace / "cli.txt").exists(), "拒绝后不得调用二进制")
def test_T2b_reject_literal_default_data(self):
d = self.runtime("ub2b")
(d / "bin").mkdir(parents=True)
shutil.copy2(self.stub, d / "bin" / "agent-system")
(d / "svc.env").write_text("".join("%s=%s\n" % kv for kv in [
("kind", "agent"), ("env", "ub2b"), ("pid", ""),
("addr", "127.0.0.1:9443"), ("tags", ""),
("source", "local@deadbee"), ("version", "stub"), ("started", ""),
("pidcheck", str(d / "bin" / "agent-system")),
("exec", "--addr 127.0.0.1:9443 --data ./data --plugins %s" % (d / "plugins")),
]))
env = dict(self.env, SVC_UB2B_DATA="./data")
result = self.svc("ub2b", "user", "list", check=False, env=env)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(any(k in result.stdout for k in ("未绑定数据目录", "不回落")),
result.stdout)
# ---- T3 删除 data 根:OpenExisting 失败,不建空库 ----
def test_T3_missing_data_root_rejected(self):
self.seed_agent("ub3", 9444)
self.start_ok("ub3")
self.svc("ub3", "stop")
root = Path(self.manifest("ub3")["identity"]["data_root"])
self.touch_db("ub3")
shutil.rmtree(root)
self.assertFalse(root.exists())
result = self.svc("ub3", "user", "create", "--owner", "x", "--name", "x",
check=False)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(any(k in result.stdout for k in ("既有库不存在", "OpenExisting")),
result.stdout)
self.assertFalse(root.exists(), "拒绝后不得重建数据根")
self.assertFalse((root / "agent-system.db").exists())
# ---- T4 隐藏 flock:user/deploy 拒绝;已运行服务仍在 ----
def test_T4_no_flock_rejects_user_and_deploy(self):
self.seed_agent("ub4", 9445)
self.start_ok("ub4")
self.assertTrue(self.running("ub4"))
self.touch_db("ub4")
# 构造不含 flock 的 PATH:链入常用外部命令,刻意排除 flock。
bin_dir = self.root / "nobin"
bin_dir.mkdir()
for name in (
"bash", "python3", "python", "dirname", "basename", "tr", "cksum",
"cut", "mkdir", "cat", "sed", "grep", "head", "rm", "mv", "cp",
"ls", "chmod", "uname", "date", "sleep", "kill", "ps", "awk",
"sort", "uniq", "env", "printf", "realpath", "readlink", "stat",
"tee", "wc", "find", "xargs", "touch", "ln", "mktemp", "id",
"git", "make", "tar", "gzip", "which", "command",
):
src = shutil.which(name)
if src:
dest = bin_dir / name
if not dest.exists():
dest.symlink_to(src)
self.assertIsNone(shutil.which("flock", path=str(bin_dir)))
env = dict(self.env, PATH=str(bin_dir))
r_user = self.svc("ub4", "user", "list", check=False, env=env)
self.assertNotEqual(r_user.returncode, 0)
self.assertIn("缺少 flock", r_user.stdout)
r_dep = self.svc("ub4", "deploy", "--dry-run", check=False, env=env)
self.assertNotEqual(r_dep.returncode, 0, r_dep.stdout)
self.assertIn("缺少 flock", r_dep.stdout)
self.assertTrue(self.running("ub4"), "拒绝后旧服务应仍在跑")
self.svc("ub4", "stop")
# ---- T5 recovery U1 身份冒烟(完整套件见 test_svc_identity)----
def test_T5_identity_regression_smoke(self):
self.seed_agent("ub5", 9446)
self.start_ok("ub5")
iid = self.manifest("ub5")["instance_id"]
self.svc("ub5", "stop")
self.start_ok("ub5")
self.assertEqual(self.manifest("ub5")["instance_id"], iid)
self.svc("ub5", "stop")
if __name__ == "__main__":
unittest.main()