Filter dependency resolution by the mod pack's Factorio version (#974)

* Filter dependency resolution by the mod pack's Factorio version

The dependency resolver picked the newest release satisfying the version
range regardless of which Factorio version it targeted, so a 2.0 mod pack
could end up with 2.1 dependencies. Only consider portal releases and
local mods whose factorio_version matches the mod pack.

Fixes #965

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Move dependency resolver tests to test/messages/mod.test.js

Also update the existing local mod fixtures to carry a factorio_version
and drop enable-all-feature-flags from the live expectations: the 2.0
releases of pyalternativeenergy only list it as optional, it was only
pulled in before because 2.1 releases were being selected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cooldude2606andClaude Fable 5 authored and GitHub committed 2026-08-19 15:21:21 +01:00
commit 00ce7cdca6
2 files changed
+81 -6

No files matched your search

+12 -3
View File
@@ -713,9 +713,14 @@ export default class ControlConnection extends BaseConnection {
return null;
}
// Select the latest matching version
// Select the latest matching version for the mod pack's Factorio version
const release = releases.releases
.filter(info => versionRange.testVersion(info.version))
.filter(info => (
versionRange.testVersion(info.version)
&& lib.normaliseMajorMinorVersion(
lib.normaliseSourceVersion(info.info_json.factorio_version)
) === factorioVersion
))
.reduce<ModPortalReleaseType | undefined>((max, cur) => (
max && lib.integerSourceVersion(max.version) > lib.integerSourceVersion(cur.version) ? max : cur
), undefined);
@@ -797,7 +802,11 @@ export default class ControlConnection extends BaseConnection {
// Check if a local version fits the requirements
let candidate = localInfos
.filter(info => (info.name === mod.name && versionRange.testVersion(info.version)))
.filter(info => (
info.name === mod.name
&& info.factorioVersion === factorioVersion
&& versionRange.testVersion(info.version)
))
.reduce<lib.ModInfo | undefined>((max, cur) => (
max && max.integerVersion > cur.integerVersion ? max : cur
), undefined);
+69 -3
View File
@@ -384,8 +384,10 @@ describe("messages/mod", function() {
}
});
it("prefers the locally installed version if matching", async function() {
controller.modStore.addMod(lib.ModInfo.fromJSON({ name: "foo", version: "1.0.0" }));
for (const factorioVersion of factorioVersions) {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: factorioVersion,
}));
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
setPortalModRelease("foo", "2.0.0", factorioVersion, []);
@@ -402,8 +404,10 @@ describe("messages/mod", function() {
}
});
it("prefers the mod portal version when checking for updates", async function() {
controller.modStore.addMod(lib.ModInfo.fromJSON({ name: "foo", version: "1.0.0" }));
for (const factorioVersion of factorioVersions) {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: factorioVersion,
}));
setPortalModRelease("root", "1.0.0", factorioVersion, ["foo"]);
setPortalModRelease("foo", "2.0.0", factorioVersion, []);
@@ -455,6 +459,68 @@ describe("messages/mod", function() {
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0", "bar_1.5.0"]));
}
});
it("only selects portal releases matching the factorio version", async function() {
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
ModReleases.set("foo", {
name: "foo",
releases: [{
version: "1.0.0", info_json: { factorio_version: "1.1", dependencies: [] },
}, {
version: "2.0.0", info_json: { factorio_version: "2.0", dependencies: [] },
}, {
version: "3.0.0", info_json: { factorio_version: "2.1", dependencies: [] },
}],
});
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
});
it("only selects local mods matching the factorio version", async function() {
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "1.0.0", factorio_version: "2.0",
}));
controller.modStore.addMod(lib.ModInfo.fromJSON({
name: "foo", version: "3.0.0", factorio_version: "2.1",
}));
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
setPortalModRelease("foo", "2.0.0", "2.0", []);
let result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
let depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0", true)
);
depIds = new Set(result.dependencies.map(mod => mod.id));
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
});
it("highlights dependencies with no release for the factorio version", async function() {
setPortalModRelease("root", "1.0.0", "2.0", ["foo"]);
setPortalModRelease("foo", "1.0.0", "2.1", []);
const result = await controlConnection.handleModDependencyResolveRequest(
new lib.ModDependencyResolveRequest([new ModDependency("root")], "2.0")
);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["foo", "notFound"],
]));
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0].id, "root_1.0.0");
});
it("rejects when a network error occurs", async function() {
lib.ModStore.fetchModReleases = function() {
throw new Error("Mock network error");
@@ -484,7 +550,7 @@ describe("messages/mod", function() {
"pyalienlife", "pyalienlifegraphics", "pyalienlifegraphics2", "pyalienlifegraphics3",
"pycoalprocessing", "pycoalprocessinggraphics", "pyfusionenergy", "pyfusionenergygraphics",
"pypetroleumhandling", "pypetroleumhandlinggraphics", "pyrawores", "pyraworesgraphics",
"pyhightech", "pyhightechgraphics", "pyindustry", "pyindustrygraphics", "enable-all-feature-flags",
"pyhightech", "pyhightechgraphics", "pyindustry", "pyindustrygraphics",
]));
});
});