Phase 0–7 of docs/joy-cli-hardening-plan.md: - golden fixtures + contract tests (commands/dashboard keys/whitelist) - command tier metadata; help ≡ manifest (pipeline in core) - _joy.schema_version on dict outputs; dashboard collect package split - work_rebuild → shared rebuild; deprecated markers; journal vm-first compat - web JSONL audit; lazy command import; plugins README
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
"""契约测试:manifest/help 同源、golden 键、schema、白名单不减。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
SRC = Path(__file__).parent.parent / "src"
|
||
FIX = Path(__file__).parent / "fixtures"
|
||
sys.path.insert(0, str(SRC))
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def commands_loaded():
|
||
from joy.cli import ensure_commands_loaded, COMMANDS
|
||
|
||
ensure_commands_loaded()
|
||
return COMMANDS
|
||
|
||
|
||
def test_command_count_not_below_golden(commands_loaded):
|
||
inv = json.loads((FIX / "command_inventory_v1.json").read_text())
|
||
golden = set(inv["commands"])
|
||
current = set(commands_loaded.keys())
|
||
missing = golden - current
|
||
assert not missing, f"命令从注册表消失: {sorted(missing)}"
|
||
assert len(current) >= len(golden)
|
||
|
||
|
||
def test_manifest_help_same_names(commands_loaded):
|
||
from joy.commands.help import _build_help_data
|
||
from joy.commands.tools import Tools
|
||
|
||
help_names = set()
|
||
for cat in _build_help_data()["categories"]:
|
||
for c in cat["commands"]:
|
||
help_names.add(c["name"])
|
||
reg = set(commands_loaded.keys())
|
||
assert help_names == reg, f"help/reg drift +{reg-help_names} -{help_names-reg}"
|
||
|
||
man = Tools()._manifest()
|
||
man_names = {c["name"] for c in man["commands"]}
|
||
assert man_names == reg
|
||
|
||
|
||
def test_pipeline_in_core(commands_loaded):
|
||
assert "pipeline" in commands_loaded
|
||
assert commands_loaded["pipeline"].tier == "core" or commands_loaded["pipeline"].tier in (
|
||
"core",
|
||
None,
|
||
)
|
||
from joy.core.registry_meta import CORE_COMMANDS, tier_for
|
||
|
||
assert "pipeline" in CORE_COMMANDS
|
||
assert tier_for("pipeline") == "core"
|
||
|
||
|
||
def test_dashboard_keys_golden():
|
||
golden = set(json.loads((FIX / "dashboard_keys_v1.json").read_text())["keys"])
|
||
from joy.dashboard.schema import DASHBOARD_TOP_LEVEL_KEYS
|
||
|
||
assert set(DASHBOARD_TOP_LEVEL_KEYS) >= golden
|
||
# _joy is meta, not required in golden business keys
|
||
assert "ts" in DASHBOARD_TOP_LEVEL_KEYS
|
||
assert "pulse" in DASHBOARD_TOP_LEVEL_KEYS
|
||
|
||
|
||
def test_web_whitelist_not_shrunk():
|
||
golden = json.loads((FIX / "web_action_prefixes_v1.json").read_text())
|
||
from joy.commands.web import ACTION_PREFIXES, ACTION_WHITELIST
|
||
|
||
cur_wl = set(ACTION_WHITELIST.keys())
|
||
cur_px = {p[0] for p in ACTION_PREFIXES}
|
||
missing_wl = set(golden["whitelist"]) - cur_wl
|
||
missing_px = set(golden["prefixes"]) - cur_px
|
||
assert not missing_wl, f"白名单减少: {sorted(missing_wl)}"
|
||
assert not missing_px, f"前缀减少: {sorted(missing_px)}"
|
||
|
||
|
||
def test_with_joy_meta_additive():
|
||
from joy.core.schema import with_joy_meta
|
||
|
||
d = with_joy_meta({"a": 1}, command="patrol")
|
||
assert d["a"] == 1
|
||
assert d["_joy"]["schema_version"] == 1
|
||
assert d["_joy"]["command"] == "patrol"
|
||
# list 不变
|
||
assert with_joy_meta([1, 2]) == [1, 2]
|
||
|
||
|
||
def test_deprecated_commands_marked(commands_loaded):
|
||
for name in ("prlctl", "clone_restart", "work_rebuild"):
|
||
assert name in commands_loaded
|
||
assert commands_loaded[name].tier == "deprecated"
|
||
assert commands_loaded[name].replaced_by
|
||
|
||
|
||
def test_work_rebuild_shared():
|
||
from joy.commands.work import rebuild_work_session
|
||
from joy.commands.work_rebuild import WorkRebuild
|
||
|
||
# 模块可导入;SSH 不可达时返回 error 结构
|
||
r = rebuild_work_session("__no_such_vm_xyz__")
|
||
assert isinstance(r, dict)
|
||
assert r.get("vm") == "__no_such_vm_xyz__"
|