"""职业生成与更新保持稳定引用,并保留手写计算规则。""" import ast import json from pathlib import Path import runpy import pytest ROOT = Path(__file__).resolve().parents[3] MERGER = runpy.run_path(str(ROOT / 'scripts/migrate_generated_characters.py')) IMPORT = 'from core.character.refs.extra.creator import CreatorSkillRef as Ref\n' def merge(tmp_path, runtime, generated): old, new = tmp_path / 'runtime.py', tmp_path / 'generated.py' old.write_text(runtime) new.write_text(generated) return MERGER['merge'](new, old) def block(name='名称', ref='Ref.CREATESPACE', uuid='uuid1', number=0, extra=''): return (f'class Skill{number}(PassiveSkill):\n name = {name!r}\n' + (f' ref = {ref}\n' if ref else '') + (f' uuid = {uuid!r}\n' if uuid else '') + ' position = 1\n cellPos = (1, 1)\n' + extra + '\n') def test_generate_class_uses_shared_member_normalization(monkeypatch): scripts = ROOT / 'backend/openapi/scripts' monkeypatch.syspath_prepend(str(scripts)) gen = runpy.run_path(str(scripts / 'generate.py')) job = {'name': 'gunner_female', 'jobId': 'job'} profession = {'name': 'launcher_female'} base = ROOT / 'backend/openapi/static/data/cn/gunner_female/launcher_female' row = next(x for x in json.loads((base / 'skill_tree.json').read_text()) if x['code'] == 'apg-63') detail = json.loads((base / 'skillDetail' / f"{row['skillId']}.json").read_text()) skill = gen['convert_data'](detail, row['skillId'], job) output = gen['generate_class'](skill, 0, row, job, profession) assert 'ref = Ref.APG_63' in output ast.parse(output) with pytest.raises(ValueError, match='code'): gen['generate_class'](skill, 0, {**row, 'code': None}, job, profession) def test_renamed_skill_matches_ref_and_keeps_rules_and_alias(tmp_path): runtime = IMPORT.replace('as Ref', 'as OldRef') + block('旧名称', 'OldRef.CREATESPACE', number=27, extra=' associate = ["手写规则"]\n level_source = OldRef.THETREEOFSEFIROT\n def custom(self):\n return 42\n') generated = IMPORT + block('新名称', extra=' data0 = [0, 10]\n') output, stats = merge(tmp_path, runtime, generated) assert stats['matched'] == 1 assert 'class Skill27(' in output assert 'ref = OldRef.CREATESPACE' in output assert 'level_source = OldRef.THETREEOFSEFIROT' in output assert 'return 42' in output and '手写规则' in output assert "name = '旧名称'" in output assert 'data0 = [0, 10]' in output ast.parse(output) def test_old_uuid_matches_after_rename_and_gets_ref_import(tmp_path): output, stats = merge(tmp_path, block('旧名称', ref=None), IMPORT + block('新名称')) assert stats['matched'] == 1 assert IMPORT.strip() in output assert 'ref = Ref.CREATESPACE' in output output2, _ = merge(tmp_path, output, IMPORT + block('新名称')) assert output == output2 def test_runtime_only_skills_survive_and_new_skills_stay_commented(tmp_path): old = IMPORT + block() + block('手写技能', ref='Ref.BACKSTEP', uuid='manual', number=9, extra=' def effect(self, old, new):\n return 42\n') new = IMPORT + block() + block('新增技能', ref='Ref.THETREEOFSEFIROT', uuid='new', number=1) output, stats = merge(tmp_path, old, new) assert stats['runtime_only'] == stats['commented'] == 1 assert 'class Skill9(' in output and 'return 42' in output assert '# class Skill1(' in output assert len([x for x in ast.parse(output).body if isinstance(x, ast.ClassDef)]) == 2 @pytest.mark.parametrize(('old_ref', 'old_uuid', 'new_ref', 'new_uuid'), [ ('Ref.CREATESPACE', 'one', 'Ref.THETREEOFSEFIROT', 'one'), (None, 'one', None, 'two'), ]) def test_identity_conflicts_do_not_fall_back_to_name(tmp_path, old_ref, old_uuid, new_ref, new_uuid): with pytest.raises(ValueError, match='冲突'): merge(tmp_path, IMPORT + block(ref=old_ref, uuid=old_uuid), IMPORT + block(ref=new_ref, uuid=new_uuid)) def test_duplicate_refs_fail_before_merge(tmp_path): with pytest.raises(ValueError, match='duplicate ref_key'): merge(tmp_path, IMPORT + block() + block('另一个技能', uuid='uuid2', number=1), IMPORT + block()) def test_legacy_name_fallback_without_identity(tmp_path): output, stats = merge(tmp_path, block(ref=None, uuid=None), block(ref=None, uuid=None)) assert stats['matched'] == 1 ast.parse(output)