check_names: add --fix-ids to repair wrong steam_ids in meta.cpp

ID_COLLISION status (previously NOT_ON_SERVER) now has its own label
and shows the bad steam_id in the report output.

--fix-ids cross-references comparison.json to find the correct
publishedid for each colliding folder and rewrites it in-place in
meta.cpp using regex, preserving all other content.

New helpers: _lookup_detailed (returns server_name + local_sid tuple),
_write_steam_id (regex-replace publishedid in meta.cpp),
_build_comparison_id_map (builds normalized_name -> steam_id map
from comparison.json).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
revernomad17
2026-04-07 17:21:21 +07:00
parent 2ab6d87532
commit 9dea44fa3d
2 changed files with 329 additions and 122 deletions

View File

@@ -1064,7 +1064,11 @@ test("all exported symbols are callable or types", _test_exports_are_callable)
group("check_names helpers")
from check_names import _server_name_from_url, _read_local_steam_id, _lookup_server_name, _resolve_status
from check_names import (
_server_name_from_url, _read_local_steam_id,
_lookup_detailed, _resolve_status, _write_steam_id,
_build_comparison_id_map,
)
def _test_server_name_from_url():
@@ -1091,7 +1095,7 @@ def _test_read_local_steam_id_no_id_in_file():
assert _read_local_steam_id(d) is None
def _test_lookup_server_name_by_steam_id():
def _test_lookup_detailed_by_steam_id():
index = {
"by_steam_id": {"463939057": "https://x.com/@ace3/"},
"by_name": {},
@@ -1099,23 +1103,23 @@ def _test_lookup_server_name_by_steam_id():
with tempfile.TemporaryDirectory() as d:
d = Path(d)
(d / "meta.cpp").write_text("publishedid = 463939057;", encoding="utf-8")
result = _lookup_server_name("@ACE3", d, index)
assert_eq(result, "@ace3")
server_name, local_sid = _lookup_detailed("@ACE3", d, index)
assert_eq(server_name, "@ace3")
assert_eq(local_sid, "463939057")
def _test_lookup_server_name_by_name_fallback():
def _test_lookup_detailed_by_name_fallback():
index = {
"by_steam_id": {},
"by_name": {"cbaa3": "https://x.com/@cba_a3/"},
}
with tempfile.TemporaryDirectory() as d:
# No meta.cpp — name fallback
result = _lookup_server_name("@CBA_A3", Path(d), index)
assert_eq(result, "@cba_a3")
server_name, local_sid = _lookup_detailed("@CBA_A3", Path(d), index)
assert_eq(server_name, "@cba_a3")
assert local_sid is None # no meta.cpp
def _test_lookup_server_name_steam_id_beats_name():
"""steam_id lookup must take priority over name fallback."""
def _test_lookup_detailed_steam_id_beats_name():
index = {
"by_steam_id": {"111": "https://x.com/@correct/"},
"by_name": {"mymod": "https://x.com/@wrong/"},
@@ -1123,55 +1127,149 @@ def _test_lookup_server_name_steam_id_beats_name():
with tempfile.TemporaryDirectory() as d:
d = Path(d)
(d / "meta.cpp").write_text("publishedid = 111;", encoding="utf-8")
result = _lookup_server_name("@MyMod", d, index)
assert_eq(result, "@correct")
server_name, local_sid = _lookup_detailed("@MyMod", d, index)
assert_eq(server_name, "@correct")
assert_eq(local_sid, "111")
def _test_lookup_server_name_not_found():
def _test_lookup_detailed_not_found():
index = {"by_steam_id": {}, "by_name": {}}
with tempfile.TemporaryDirectory() as d:
result = _lookup_server_name("@Unknown", Path(d), index)
assert result is None
server_name, local_sid = _lookup_detailed("@Unknown", Path(d), index)
assert server_name is None
test("_server_name_from_url: extracts name from URL", _test_server_name_from_url)
test("_read_local_steam_id: reads meta.cpp", _test_read_local_steam_id_present)
test("_read_local_steam_id: no meta.cpp -> None", _test_read_local_steam_id_missing)
test("_read_local_steam_id: meta.cpp without id -> None", _test_read_local_steam_id_no_id_in_file)
test("_lookup_server_name: matches by steam_id", _test_lookup_server_name_by_steam_id)
test("_lookup_server_name: falls back to name", _test_lookup_server_name_by_name_fallback)
test("_lookup_server_name: steam_id beats name fallback", _test_lookup_server_name_steam_id_beats_name)
test("_lookup_server_name: not found -> None", _test_lookup_server_name_not_found)
def _test_lookup_detailed_returns_sid_even_when_not_on_server():
"""local_sid should be returned even when server lookup fails."""
index = {"by_steam_id": {}, "by_name": {}}
with tempfile.TemporaryDirectory() as d:
d = Path(d)
(d / "meta.cpp").write_text("publishedid = 999;", encoding="utf-8")
server_name, local_sid = _lookup_detailed("@SomeMod", d, index)
assert server_name is None
assert_eq(local_sid, "999")
# _resolve_status tests (the false-positive filter)
test("_lookup_detailed: matches by steam_id", _test_lookup_detailed_by_steam_id)
test("_lookup_detailed: falls back to name", _test_lookup_detailed_by_name_fallback)
test("_lookup_detailed: steam_id beats name fallback", _test_lookup_detailed_steam_id_beats_name)
test("_lookup_detailed: not found -> None", _test_lookup_detailed_not_found)
test("_lookup_detailed: returns local_sid even when not on server", _test_lookup_detailed_returns_sid_even_when_not_on_server)
# _resolve_status tests
def _test_resolve_status_ok():
status, col = _resolve_status("@ace", "@ace", ok_disk_names={"@ace"})
status, col = _resolve_status("@ace", "@ace", None, ok_disk_names={"@ace"})
assert_eq(status, "OK")
assert_eq(col, "@ace")
def _test_resolve_status_mismatch():
status, col = _resolve_status("@ACE3", "@ace3", ok_disk_names={"@cba_a3"})
status, col = _resolve_status("@ACE3", "@ace3", None, ok_disk_names={"@cba_a3"})
assert_eq(status, "MISMATCH")
assert_eq(col, "@ace3")
def _test_resolve_status_not_found():
status, col = _resolve_status("@Unknown", None, ok_disk_names=set())
status, col = _resolve_status("@Unknown", None, None, ok_disk_names=set())
assert_eq(status, "NOT_ON_SERVER")
def _test_resolve_status_steam_id_collision():
# server_name is already correctly held by another folder → false positive
def _test_resolve_status_id_collision():
ok = {"@Realistic Ragdoll Physics"}
status, col = _resolve_status("@NIArms All in One- ACE Compatibility",
"@Realistic Ragdoll Physics",
ok_disk_names=ok)
assert_eq(status, "NOT_ON_SERVER", "Should be reclassified due to collision")
assert "collision" in col
status, col = _resolve_status(
"@NIArms All in One- ACE Compatibility",
"@Realistic Ragdoll Physics",
"1234567",
ok_disk_names=ok,
)
assert_eq(status, "ID_COLLISION")
assert "1234567" in col # bad steam_id shown in output
def _test_resolve_status_collision_without_sid():
ok = {"@Realistic Ragdoll Physics"}
status, col = _resolve_status(
"@Some Mod", "@Realistic Ragdoll Physics", None, ok_disk_names=ok
)
assert_eq(status, "ID_COLLISION")
test("_resolve_status: OK match", _test_resolve_status_ok)
test("_resolve_status: genuine mismatch", _test_resolve_status_mismatch)
test("_resolve_status: not found", _test_resolve_status_not_found)
test("_resolve_status: steam_id collision reclassified as NOT_ON_SERVER", _test_resolve_status_steam_id_collision)
test("_resolve_status: ID_COLLISION shows bad steam_id", _test_resolve_status_id_collision)
test("_resolve_status: ID_COLLISION without local sid", _test_resolve_status_collision_without_sid)
# _write_steam_id tests
def _test_write_steam_id_updates_existing():
with tempfile.TemporaryDirectory() as d:
d = Path(d)
meta = d / "meta.cpp"
meta.write_text('publishedid = 111;\nname = "OldMod";\n', encoding="utf-8")
ok, msg = _write_steam_id(d, "999")
assert ok
assert "999" in meta.read_text(encoding="utf-8")
assert "111" not in meta.read_text(encoding="utf-8")
assert 'name = "OldMod"' in meta.read_text(encoding="utf-8") # preserved
def _test_write_steam_id_creates_if_missing():
with tempfile.TemporaryDirectory() as d:
d = Path(d)
ok, msg = _write_steam_id(d, "12345")
assert ok
meta = d / "meta.cpp"
assert meta.exists()
assert "12345" in meta.read_text(encoding="utf-8")
def _test_write_steam_id_appends_if_no_line():
with tempfile.TemporaryDirectory() as d:
d = Path(d)
meta = d / "meta.cpp"
meta.write_text('name = "NoId";\n', encoding="utf-8")
ok, msg = _write_steam_id(d, "777")
assert ok
content = meta.read_text(encoding="utf-8")
assert "777" in content
assert 'name = "NoId"' in content
test("_write_steam_id: updates existing publishedid", _test_write_steam_id_updates_existing)
test("_write_steam_id: creates meta.cpp if missing", _test_write_steam_id_creates_if_missing)
test("_write_steam_id: appends if no publishedid line", _test_write_steam_id_appends_if_no_line)
# _build_comparison_id_map tests
def _test_build_comparison_id_map_reads_json():
comparison = {
"compared_presets": ["A", "B"],
"shared": {"mod_count": 1, "mods": [
{"name": "CBA_A3", "steam_id": "450814997", "url": None, "source": "steam"},
]},
"unique": {
"A": {"mod_count": 1, "mods": [
{"name": "ACE3", "steam_id": "463939057", "url": None, "source": "steam"},
]},
"B": {"mod_count": 0, "mods": []},
},
}
with tempfile.TemporaryDirectory() as d:
cfg_data = {
"server": {"base_url": "x", "username": "u", "password": "p"},
"paths": {"arma_dir": d, "downloads": d,
"modlist_html": d, "modlist_json": d},
}
import json as _json
cfg_file = Path(d) / "config.json"
cfg_file.write_text(_json.dumps(cfg_data), encoding="utf-8")
from arma_modlist_tools.config import load_config as _lc
cfg = _lc(str(cfg_file))
# Write a fake comparison.json
comp_path = Path(d) / "comparison.json"
comp_path.write_text(_json.dumps(comparison), encoding="utf-8")
id_map = _build_comparison_id_map(cfg)
assert_in("cbaa3", id_map)
assert_eq(id_map["cbaa3"][0], "450814997")
assert_in("ace3", id_map)
assert_eq(id_map["ace3"][0], "463939057")
test("_build_comparison_id_map: reads steam_ids from comparison.json", _test_build_comparison_id_map_reads_json)
# ---------------------------------------------------------------------------