from pathlib import Path import re import subprocess from hockey_data.agent_bridge import _stable_vmix_inventory_fingerprint ROOT = Path(__file__).resolve().parents[1] def test_stable_project_fingerprint_ignores_input_number_order_and_title_when_key_exists(): before = [ { "key": "key-score", "number": "22", "title": "Верхний счёт", "type": "GT", "fields": [ {"name": "Таймер.Text", "type": "text", "index": "0"}, {"name": "HOME.Text", "type": "text", "index": "1"}, ], }, { "key": "key-penalty", "number": "114", "title": "Удаление HOME", "type": "GT", "fields": [{"name": "Таймер.Text", "type": "text", "index": "0"}], }, ] after = [ { "key": "key-penalty", "number": "7", "title": "Удаление HOME (новое имя)", "type": "GT", "fields": [{"name": "Таймер.Text", "type": "text", "index": "4"}], }, { "key": "key-score", "number": "91", "title": "Счёт", "type": "GT", "fields": [ {"name": "HOME.Text", "type": "text", "index": "9"}, {"name": "Таймер.Text", "type": "text", "index": "2"}, ], }, ] assert _stable_vmix_inventory_fingerprint(before) == _stable_vmix_inventory_fingerprint(after) def test_legacy_project_fingerprint_uses_title_instead_of_number(): before = [{"key": "", "number": "3", "title": "Score", "type": "GT", "fields": []}] after = [{"key": "", "number": "99", "title": "Score", "type": "GT", "fields": []}] assert _stable_vmix_inventory_fingerprint(before) == _stable_vmix_inventory_fingerprint(after) def _run_functions(source, names, scenario): # Execute the shipped function bodies; test doubles supply only page state. functions = [] for name in names: match = re.search(r"(?m)^ function " + re.escape(name) + r"\(", source) assert match, name following = re.search(r"(?m)^ (?:async )?function ", source[match.end():]) assert following, name functions.append(source[match.start():match.end() + following.start()]) script = "const assert = require('node:assert/strict');\n" + "\n".join(functions) + "\n" + scenario result = subprocess.run(["node", "-e", script], text=True, capture_output=True, timeout=10) assert result.returncode == 0, result.stderr def test_shortcut_editor_persists_title_and_migrates_legacy_selectors(): app_js = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8") _run_functions(app_js, ["vmixInventoryInputs", "vmixInputRef", "vmixInputByRef"], r''' const score = {key: 'score-guid', number: '5', title: 'Score'}; const other = {key: 'other-guid', number: '9', title: 'Other'}; const state = {shortcutInventory: {inventory: {inputs: [score, other]}}}; assert.equal(vmixInputRef(score), 'Score'); assert.equal(vmixInputRef({key: 'only-guid', number: '5'}), ''); for (const legacy of ['score-guid', '5']) assert.equal(vmixInputRef(vmixInputByRef(legacy)), 'Score'); const saved = vmixInputRef(score); score.number = '9'; other.number = '5'; score.key = 'new-score-guid'; assert.equal(vmixInputByRef(saved), score); assert.equal(vmixInputRef(vmixInputByRef(saved)), 'Score'); assert.equal(vmixInputByRef('missing'), null); ''') def test_mapping_links_save_exact_title_and_clear_every_positional_number(): mapping_js = (ROOT / "hockey_data" / "static" / "admin-directories.js").read_text(encoding="utf-8") _run_functions(mapping_js, ["mappingInventoryInputs", "mappingFieldKind", "mappingRowMatchesInput", "mappingInputTitleProblem", "mappingSetLink", "collectMappingFields"], r''' const input = {key: 'current-guid', number: '17', title: 'Score', fields: [{name: 'Home.Text', type: 'text'}]}; const profile = {inventory: {inputs: [input]}, fields: [{data_key:'old', vmix_input_key:'old-guid', vmix_input_number:'5', vmix_input_title:'Score', vmix_field:'Home.Text', rule:{enabled:true, left_key:'old'}}]}; const state = {mappingActiveProfile:profile}; const statuses = []; function setStatus(message){statuses.push(message);} mappingSetLink(profile, input, input.fields[0], {key:'score.home'}); assert.equal(profile.fields.length, 1); assert.equal(profile.fields[0].vmix_input_title, 'Score'); assert.equal(profile.fields[0].vmix_input_key, 'current-guid'); assert.equal(profile.fields[0].vmix_input_number, ''); assert.equal(profile.fields[0].rule.left_key, 'score.home'); profile.fields.push({data_key:'legacy', vmix_input_key:'another-guid', vmix_input_number:'42', vmix_input_title:'Other', vmix_field:'Away.Text'}); profile.fields.push({data_key:'orphaned-legacy', vmix_input_key:'', vmix_input_number:'7', vmix_input_title:'', vmix_field:'Legacy.Text'}); const collected = collectMappingFields(); assert.deepEqual(collected.map(row=>row.vmix_input_number), ['', '', '']); assert.deepEqual(collected.map(row=>row.vmix_input_title), ['Score', 'Other', '']); assert.deepEqual(collected.map(row=>row.sort_order), [0, 1, 2]); assert.equal(profile.fields[1].vmix_input_number, '42', 'Collecting must not mutate draft rows'); const previous = JSON.stringify(profile.fields); profile.inventory.inputs.push({...input, key:'duplicate-guid', number:'18'}); mappingSetLink(profile, input, input.fields[0], {key:'unsafe'}); assert.equal(JSON.stringify(profile.fields), previous); assert.ok(statuses.length && statuses[0].includes('не уникально')); ''')