import base64 import struct import zlib from contextlib import nullcontext import pytest from core.open import skillTree from api.utils.skill_tree import load_skill_tree_map from core.open.skillTree import decode_skill_hash, get_openapi_skill_tree_info, get_skill_tree_info @pytest.fixture(autouse=True) def no_live_redis(monkeypatch): monkeypatch.setattr(skillTree, 'get_redis_client', lambda: nullcontext(None)) def encode(skills=(), job=(0, 3), remaining=b''): header = bytearray(14) header[0] = 0xF3 header[2:4] = bytes(job) header[5] = 115 header[8] = ord('K') data = struct.pack('> 8)) # 无进化/强化,两个快捷栏:鬼斩、空槽。 data += bytes([0, 0, 0, 2, 5, 0, 0, 0]) + remaining return base64.b64encode(zlib.compress(header + data)).decode() def test_decode_adds_missing_auto_skills_without_overwriting_encoded_levels(): result = decode_skill_hash(encode([(5, 7)])) assert result['skills'][0]['level'] == 7 automatic = {item['skill_index']: item['level'] for item in result['auto_skills']} tree = load_skill_tree_map('swordman_male', 'berserker') assert automatic == { int(key): item['autoLevel'] for key, item in tree.items() if 'autoLevel' in item and key != '5' } assert automatic[174] == 115 assert 'arrangements' not in result def test_openapi_result_does_not_create_character(monkeypatch): import core.basic.character as character def fail(*args, **kwargs): raise AssertionError('must not create a character') monkeypatch.setattr(character, 'createCharacter', fail) result = get_openapi_skill_tree_info(encode()) assert result['skills']['5']['lv'] == 1 assert result['shortcuts'] == [ {'slot': 0, 'skill_id': 5, 'skill_name': '鬼斩'}, {'slot': 1, 'skill_id': None, 'skill_name': None}, ] def test_calculator_receives_automatic_levels_and_shortcuts(): result = get_skill_tree_info(encode()) skills = {item['name']: item for item in result['skills'].values()} assert skills['鬼斩']['lv'] == 1 assert skills['基础精通']['lv'] == 115 assert result['shortcuts'][0]['skill_name'] == '鬼斩' def test_dark_knight_arrangements_and_remaining_are_preserved(): remaining = bytes([1, 118, 0, 1, 60, 0]) result = decode_skill_hash(encode(job=(9, 0), remaining=remaining)) assert result['arrangements'] == [{'slot_index': 118, 'skills': [60]}] assert result['section2_remaining'] == remaining assert result['auto_skills'] def test_context_uses_separate_redis_map_cache(monkeypatch): from config.main import config monkeypatch.setattr(config, 'DEBUG_MODE', False) class FakeRedis: value = None keys = [] def get(self, key): self.keys.append(key) return self.value def set(self, key, value): self.value = value redis = FakeRedis() monkeypatch.setattr(skillTree, 'get_redis_client', lambda: nullcontext(redis)) header = {'job_bytes': (0, 3)} _, original = skillTree._get_skill_tree_context(header) def fail(*args): raise AssertionError('cache hit must not load local data') monkeypatch.setattr(skillTree, 'load_skill_tree_map', fail) _, cached = skillTree._get_skill_tree_context(header) assert cached == original assert cached['174']['autoLevel'] == 115 assert redis.keys == ['openapi:skillTreeMap:v1:swordman_male:berserker:map'] * 2 @pytest.mark.parametrize(('code', 'interval_ms'), [ ('eJytjb9OwnAABu9CmpQG0MUYjBYHaCRi8ytQW/7sDiQ46OxLuMArsTCwM/lGvICBZ+Cbbrjcd4o6UeOXzQqk1Qo/26/P+Jt1O3ykLI1c2HZuk5mxtXdW3vvuLaUPTtWJN47tUpgZTM199c3c0YUSX0wYGnxSMnWg9JVHE/bIDumpqXpEDsREuRZaayWlNEZSSS2dc3ro1MyZE/8srA0uXJ7VIzugLzw7cM/1Znl5qoTTP4KaGC0=', 0), ('eJytjT9OwnAARt8LaVIb/iXGEI1WB2gkQvOrUFuE3cFEBp25hIvewLOwMLgzcSMuQOAMfNMbXt63j9pR45ufd5BmM6x+P5fxFx+t8JayMHJuy1cvmBlbe2Vlzxe7lN44VSd2fPaawsxgau6TY3NHJ0p8NGFo8E7J1IHSV25N2CBr5F5N1S3yT0yUa6G1VlJKYySV1NI+podOzZw5cWdhbXDu4qhuWQN94cGBG843y9NT5d/l/gCEWBjI', 5000), ]) def test_confirmed_skill_combo_samples(code, interval_ms): result = decode_skill_hash(code) assert result['skill_combo'] == { 'skills': [309, 305, 312, 311], 'interval_ms': interval_ms, } raw = zlib.decompress(base64.b64decode(code)) assert result['section2_remaining'] == raw[-103:] + b'\x00' def test_skill_combo_preserves_empty_slots_and_rejects_truncated_layout(): remaining = bytearray(103) remaining[92:100] = struct.pack('<4H', 98, 114, 93, 0) remaining[102] = 0xF3 original = bytes(remaining) assert skillTree._parse_skill_combo(original) == { 'skills': [98, 114, 93, None], 'interval_ms': 0, } assert bytes(remaining) == original assert skillTree._parse_skill_combo(original[:102]) is None assert skillTree._parse_skill_combo(bytes(103)) is None assert 'skill_combo' not in decode_skill_hash(encode(job=(9, 0)))