Files
agentmc/tests/test_instance.py

176 lines
7.5 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.

"""实例层回归:传入文件夹 → 拉起实例(版本 + mod + 方块表)。"""
import sys, os, json, zipfile, tempfile, shutil
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mcfold.instance import Instance
from mcfold.modjar import scan_mods
BLOCKS_MOD = "/home/minecraft/mods/[扩充乐事] expandeddelight-0.1.4.jar"
FORGE_MOD = "/home/mc-test/mods/curtain-mc1.20.1-1.3.2.jar"
REAL_INST = "/home/newqqagent/mcview/store/vanilla-1.20.1"
def build_instance(with_mods=True, version_id="1.20.1", mc=None):
"""造一个临时实例文件夹(完整 MC 客户端安装形状)。
version_id 是目录名(可能是加载器 id);mc 是它继承的原版版本。
"""
d = tempfile.mkdtemp(prefix="mcfold-inst-")
ver = version_id
vdir = os.path.join(d, "versions", ver)
os.makedirs(vdir, exist_ok=True)
obj = {"id": ver, "mainClass": "net.minecraft.client.main.Main", "assets": "5"}
if mc and mc != ver:
obj["inheritsFrom"] = mc
obj["mainClass"] = ("io.github.zekerzhayard.forgewrapper.installer.Main"
if "forge" in ver else
"net.fabricmc.loader.impl.launch.knot.KnotClient")
with open(os.path.join(vdir, f"{ver}.json"), "w") as f:
json.dump(obj, f)
os.makedirs(os.path.join(d, "mods"), exist_ok=True)
if with_mods:
for src in (BLOCKS_MOD, FORGE_MOD):
if os.path.isfile(src):
shutil.copy(src, os.path.join(d, "mods"))
return d
def test_load_from_folder():
"""★ 用户口径:传入文件夹路径 → 拉起实例。"""
d = build_instance()
try:
inst = Instance(d).load()
st = inst.status()
assert st.mc_version == "1.20.1", st.mc_version
assert st.protocol == 763, st.protocol
assert st.loader == "vanilla", st.loader
print(f" ✓ 传入文件夹拉起实例: {st.version_id} mc={st.mc_version} 协议={st.protocol}")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_mods_registered():
"""★ 读取 mod jar 注册方块表。"""
d = build_instance()
try:
inst = Instance(d).load()
ms = inst.mods_summary()
assert ms["loaded"] == 2, ms
assert ms["block_name_count"] > 20, ms
names = inst.mod_block_names()
assert "expandeddelight:cask" in names, names[:5]
# 诚实边界必须体现在摘要里
assert "state id" in ms["note"]
print(f" ✓ mod 注册: {ms['loaded']} 个 mod, {ms['block_name_count']} 个方块名")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_loader_version_normalization():
"""forge 实例:mc_version 必须归一化(否则协议选错)。"""
d = build_instance(version_id="1.20.1-forge-47.4.0", mc="1.20.1")
try:
inst = Instance(d, prefer_version="1.20.1-forge-47.4.0").load()
st = inst.status()
assert st.mc_version == "1.20.1", st.mc_version
assert st.loader == "forge", st.loader
assert st.protocol == 763, st.protocol
print(f" ✓ forge 归一化: id={st.version_id} → mc={st.mc_version} 协议={st.protocol}")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_block_name_validation():
"""方块名校验:裸名可展开;歧义不误判。"""
d = build_instance()
try:
inst = Instance(d).load()
assert inst.knows_block_name("cask")
assert inst.expand_block_name("cask") == "expandeddelight:cask"
assert inst.knows_block_name("stone")
assert not inst.knows_block_name("definitely_not_a_block")
print(" ✓ 方块名校验: 裸名展开正确, 不存在的方块判否")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_ambiguous_bare_name():
"""两个 mod 有同名方块时,裸名必须判为**不可用**(歧义)。"""
d = build_instance(with_mods=False)
try:
for mid in ("moda", "modb"):
with zipfile.ZipFile(os.path.join(d, "mods", f"{mid}.jar"), "w") as zf:
zf.writestr("fabric.mod.json",
json.dumps({"id": mid, "name": mid, "version": "1"}))
zf.writestr(f"assets/{mid}/blockstates/shared_block.json", "{}")
inst = Instance(d).load()
assert not inst.knows_block_name("shared_block"), "有歧义却判为认识"
assert inst.knows_block_name("moda:shared_block"), "限定名应可用"
print(" ✓ 歧义处理: 裸名(两 mod 同名)判否, 限定名可判是")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_real_install_folder():
"""对真实安装目录验证(无 mod 的 vanilla)。"""
if not os.path.isdir(REAL_INST):
print(" - 真实安装目录不存在,跳过"); return
inst = Instance(REAL_INST).load()
st = inst.status()
assert st.mc_version == "1.20.1" and st.protocol == 763, (st.mc_version, st.protocol)
assert inst.registry is not None and len(inst.registry.blocks) > 900
print(f" ✓ 真实安装: {st.version_id} 协议={st.protocol} 原版方块={len(inst.registry.blocks)}")
def test_resolve_block_name_mod_attribution():
"""越界 state id 的报错必须**归因到 mod**(让降级可信)。"""
d = build_instance()
try:
inst = Instance(d).load()
msg = inst.resolve_block_name(99999)
assert msg.startswith("unknown("), msg
assert "mod" in msg, msg
print(f" ✓ 越界归因: {msg[:46]}…")
finally:
shutil.rmtree(d, ignore_errors=True)
def test_e2e_connect_from_instance():
"""★ 端到端:实例文件夹 → 连服 → 登录(协议由文件夹决定,不硬编码)。"""
import socket, subprocess, time
HOST, PORT = "127.0.0.1", 25599
runtime = "/home/newqqagent/plugin_data/mc/runtime"
def reachable():
try:
socket.create_connection((HOST, PORT), timeout=2).close(); return True
except OSError:
return False
proc = None
if not reachable():
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), "support_testserver.js")
if not os.path.isfile(src):
print(" - 无 support_testserver.js,跳过"); return
shutil.copy(src, runtime)
proc = subprocess.Popen(["node", "support_testserver.js", str(PORT)], cwd=runtime,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for _ in range(50):
if reachable(): break
time.sleep(0.2)
if not reachable():
print(" - 服务端不可用,跳过")
if proc: proc.kill()
return
d = build_instance()
try:
inst = Instance(d).load()
c = inst.connect(HOST, PORT, "McfoldBot")
r = c.run_smoke()
assert r["protocol"] == 763 and r["login_ok"], r
print(f" ✓ 端到端: 文件夹→协议{r['protocol']}→登录成功 (mod {len(inst.mods)} 个)")
c.close()
finally:
shutil.rmtree(d, ignore_errors=True)
if proc:
proc.terminate()
try: proc.wait(timeout=5)
except Exception: proc.kill()
try: os.remove(os.path.join(runtime, "support_testserver.js"))
except OSError: pass
if __name__ == "__main__":
for fn in [test_load_from_folder, test_mods_registered, test_loader_version_normalization,
test_block_name_validation, test_ambiguous_bare_name, test_real_install_folder,
test_resolve_block_name_mod_attribution, test_e2e_connect_from_instance]:
fn()
print("\n实例层:全部通过")