diff-based mod removal via --previous-manifest
Adds removal-detection: when --previous-manifest <path> is given, the converter diffs the previous publish against current packwiz state and emits modify[].type=remove entries for mods/resourcepacks/etc that disappeared, using simple-mod-sync's on-disk naming convention as the regex pattern. Reverse-engineered from upstream source: - simple-mod-sync writes <sanitized_name>-<sanitized_version>.<ext> - StringUtils.sanitize strips [^a-zA-Z0-9.\-_] - GetOlderVersion() finds files starting with <name>- and auto-deletes on version bumps. So version upgrades need no converter handling; only full removals do. 8 new tests including end-to-end CLI verification with a synthetic previous manifest. 23/23 pass.
This commit is contained in:
@@ -168,6 +168,112 @@ def test_cli_missing_pack_toml(tmp_path: Path) -> None:
|
||||
assert "pack.toml" in proc.stderr
|
||||
|
||||
|
||||
def test_diff_removals_empty_when_no_changes() -> None:
|
||||
"""No removals when current sync covers everything in previous."""
|
||||
prev = [{"name": "Sodium", "type": "mod"}, {"name": "Fabric API", "type": "mod"}]
|
||||
cur = [{"name": "Sodium", "type": "mod"}, {"name": "Fabric API", "type": "mod"}]
|
||||
assert p2s.diff_removals(prev, cur) == []
|
||||
|
||||
|
||||
def test_diff_removals_emits_remove_for_dropped_mod() -> None:
|
||||
"""Mod present previously but not currently → modify[].type=remove entry."""
|
||||
prev = [{"name": "Sodium", "type": "mod"}, {"name": "Iris", "type": "mod"}]
|
||||
cur = [{"name": "Sodium", "type": "mod"}]
|
||||
rm = p2s.diff_removals(prev, cur)
|
||||
assert len(rm) == 1
|
||||
assert rm[0]["type"] == "remove"
|
||||
assert rm[0]["path"] == "."
|
||||
# pattern targets simple-mod-sync's on-disk naming
|
||||
assert rm[0]["pattern"] == r"^mods/Iris-.*\.jar$"
|
||||
|
||||
|
||||
def test_diff_removals_sanitizes_name() -> None:
|
||||
"""Name with spaces and symbols sanitized to match simple-mod-sync's filename."""
|
||||
prev = [{"name": "Fabric API (Old)", "type": "mod"}]
|
||||
cur: list[dict] = []
|
||||
rm = p2s.diff_removals(prev, cur)
|
||||
# sanitize() strips space, '(', ')'
|
||||
assert rm[0]["pattern"] == r"^mods/FabricAPIOld-.*\.jar$"
|
||||
|
||||
|
||||
def test_diff_removals_per_type_paths_and_extensions() -> None:
|
||||
"""Each content type targets the right directory + extension."""
|
||||
prev = [
|
||||
{"name": "Branding", "type": "resourcepack"},
|
||||
{"name": "Iris", "type": "shader"},
|
||||
{"name": "Worldgen", "type": "datapack"},
|
||||
]
|
||||
rm = p2s.diff_removals(prev, [])
|
||||
patterns = {r["pattern"] for r in rm}
|
||||
assert patterns == {
|
||||
r"^resourcepacks/Branding-.*\.zip$",
|
||||
r"^shaderpacks/Iris-.*\.zip$",
|
||||
r"^datapacks/Worldgen-.*\.zip$",
|
||||
}
|
||||
|
||||
|
||||
def test_convert_with_previous_manifest_adds_modify_section() -> None:
|
||||
"""When --previous-manifest is supplied to convert(), modify[] appears
|
||||
in output for any name that disappeared."""
|
||||
previous = {
|
||||
"sync_version": 3,
|
||||
"sync": [
|
||||
{"name": "Sodium", "type": "mod", "url": "x", "version": "1"},
|
||||
{"name": "Iris", "type": "mod", "url": "y", "version": "1"},
|
||||
{"name": "Some Removed Mod", "type": "mod", "url": "z", "version": "1"},
|
||||
],
|
||||
}
|
||||
r = p2s.convert(FIXTURE, previous_manifest=previous)
|
||||
assert "modify" in r.manifest
|
||||
patterns = {e["pattern"] for e in r.manifest["modify"]}
|
||||
# Fixture has Sodium + Fabric API (both kept under "mod"), so Iris + Some Removed Mod
|
||||
# should both be removed. (Iris in fixture is type=shader under "Complementary Shaders",
|
||||
# so the mod-keyed Iris from previous is genuinely gone.)
|
||||
assert r"^mods/Iris-.*\.jar$" in patterns
|
||||
assert r"^mods/SomeRemovedMod-.*\.jar$" in patterns
|
||||
|
||||
|
||||
def test_convert_without_previous_manifest_omits_modify() -> None:
|
||||
r = p2s.convert(FIXTURE)
|
||||
assert "modify" not in r.manifest
|
||||
assert r.removed_entries == []
|
||||
|
||||
|
||||
def test_cli_previous_manifest_flag(tmp_path: Path) -> None:
|
||||
"""End-to-end CLI with --previous-manifest."""
|
||||
prev = tmp_path / "old.json"
|
||||
prev.write_text(json.dumps({
|
||||
"sync_version": 3,
|
||||
"sync": [
|
||||
{"name": "Sodium", "type": "mod", "url": "x", "version": "1"},
|
||||
{"name": "GoneAway", "type": "mod", "url": "y", "version": "1"},
|
||||
],
|
||||
}))
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), str(FIXTURE), "--quiet",
|
||||
"--previous-manifest", str(prev)],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
payload = json.loads(proc.stdout)
|
||||
assert "modify" in payload
|
||||
patterns = {e["pattern"] for e in payload["modify"]}
|
||||
assert r"^mods/GoneAway-.*\.jar$" in patterns
|
||||
|
||||
|
||||
def test_cli_previous_manifest_missing_file_warns(tmp_path: Path) -> None:
|
||||
"""Non-existent previous-manifest is a warning, not an error — first run case."""
|
||||
nonexistent = tmp_path / "never-written.json"
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), str(FIXTURE),
|
||||
"--previous-manifest", str(nonexistent)],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
# Should succeed, emit manifest with no modify section, and warn on stderr.
|
||||
payload = json.loads(proc.stdout)
|
||||
assert "modify" not in payload
|
||||
assert "does not exist" in proc.stderr
|
||||
|
||||
|
||||
def test_convert_against_real_packwiz_example_pack(tmp_path: Path) -> None:
|
||||
"""Integration: clone the upstream packwiz-example-pack and convert it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user