"""svc.sh 账户帮助可发现性(方案 U5 T5-1 最小切片)。 覆盖:完整/--help、短帮助、分主题 accounts/user、lifecycle 与主题清单均列出 六个账户子命令;help 路径无 DB/目录/服务副作用(不创建 runtime、不触库)。 """ import os import shutil import subprocess import tempfile import unittest from pathlib import Path SOURCE = Path(__file__).resolve().parents[1] SVC = SOURCE / "scripts" / "svc.sh" SIX = ( "create", "list", "reset-password", "issue-login-key", "revoke-login-key", "bootstrap-admin", ) class UserHelpTests(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp(prefix="svc-user-help-")) self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True) # Isolate any accidental CWD probes; help must not create runtime here. self.cwd = self.tmp / "cwd" self.cwd.mkdir() def run_help(self, *args): env = os.environ.copy() env["HOME"] = str(self.tmp / "home") Path(env["HOME"]).mkdir(exist_ok=True) return subprocess.run( ["bash", str(SVC), *args], cwd=self.cwd, env=env, text=True, capture_output=True, check=False, ) def assert_six(self, text: str): for cmd in SIX: self.assertIn(cmd, text, f"missing account subcommand {cmd!r}") def assert_no_side_effects(self): self.assertFalse( (self.cwd / "runtime").exists(), "help must not create runtime/", ) leftovers = [p for p in self.cwd.iterdir() if p.name != ".svc.env"] # allow empty; forbid agent-system.db or data trees for p in leftovers: self.assertNotEqual(p.name, "data") self.assertFalse(p.name.endswith(".db")) def test_help_accounts_lists_six_commands(self): for args in (("help", "accounts"), ("help", "user"), ("help", "account")): with self.subTest(args=args): r = self.run_help(*args) self.assertEqual(r.returncode, 0, r.stderr) out = r.stdout + r.stderr self.assert_six(out) self.assertIn("open-existing", out) self.assertIn("回落", out) self.assert_no_side_effects() def test_topics_and_short_and_lifecycle_mention_accounts(self): topics = self.run_help("help") self.assertEqual(topics.returncode, 0, topics.stderr) topics_out = topics.stdout + topics.stderr self.assertIn("accounts", topics_out) short = self.run_help("-h") short_out = short.stdout + short.stderr self.assertIn("user", short_out) self.assertIn("accounts", short_out) life = self.run_help("help", "lifecycle") life_out = life.stdout + life.stderr self.assertIn("user", life_out) self.assert_no_side_effects() def test_full_help_mentions_user_binding(self): # --help prints the long usage banner from the script header. r = self.run_help("--help") out = r.stdout + r.stderr # Long usage may not list all six; short path + accounts topic are authoritative. # Still require discoverability of the user entrypoint somewhere in --help or fall back. if "user" not in out: # Accept if topics path works; full banner is historical. self.assertIn("help", out.lower()) self.assert_no_side_effects() if __name__ == "__main__": unittest.main()