Refactor how dep resolution errors are displayed

This commit is contained in:
Cooldude2606 committed 2026-02-09 21:35:54 +00:00
1 parent aeb0c3f5f7
commit acf711fbb0
6 files changed
+234 -213

No files matched your search

+1 -1
View File
@@ -104,7 +104,7 @@ async function main() {
removeBuildArtifacts();
removeTestArtifacts();
})
.command("fast", "Remove all build artifacts", () => {}, argv => {
.command("$0", "Remove all build artifacts", () => {}, argv => {
DRY = argv.dry;
removeBuildArtifacts();
})
+1 -3
View File
@@ -15,9 +15,7 @@
"ci-cover": "nyc --reporter=lcovonly pnpm run-script test",
"lint": "eslint packages plugins test",
"lint-fix": "eslint --fix packages plugins test",
"clean": "node ./clean.js fast",
"clean-tests": "node ./clean.js tests",
"clean-all": "node ./clean.js all",
"clean": "node ./clean.js",
"docs": "typedoc",
"bundle-dependencies": "bundle-dependencies"
},
+62 -45
View File
@@ -614,22 +614,21 @@ export default class ControlConnection extends BaseConnection {
return modInfos;
}
// eslint-disable-next-line complexity
async handleModDependencyResolveRequest(request: lib.ModDependencyResolveRequest) {
const {factorioVersion, checkForUpdates} = request;
const searchQueue = [...request.mods];
const localInfos = [...this._controller.modStore.mods()];
const _builtInModNames = lib.ModPack.getBuiltinModNames(factorioVersion);
const builtInModNames = new Set(_builtInModNames);
const skipSearch = new Set(_builtInModNames);
const builtInModNames = lib.ModPack.getBuiltinModNames(factorioVersion);
const skipSearch = new Set(builtInModNames);
type ModPortalModType = InstanceType<typeof lib.ModPortalGetAllRequest.Response>["mods"][number];
type ModPortalReleaseType = NonNullable<ModPortalModType["releases"]>[number];
const dependencyRequirements = new Map<string, lib.ModVersionRange>();
const optionalRequirements = new Map<string, lib.ModVersionRange>();
const candidateReleases = new Map<string, lib.ModInfo>();
const incompatible = new Set<string>();
const invalid = new Set<string>();
const errors = new Map<string, lib.ModDependencyResolveErrors>();
const _modPortalReleases = new Map<string, ModPortalModType | null>();
async function fetchModReleases(modName: string) {
@@ -650,10 +649,44 @@ export default class ControlConnection extends BaseConnection {
return releases;
}
function selectModRelease(releases: ModPortalModType | null, versionRange: lib.ModVersionRange) {
if (!releases || !releases.releases) {
return null;
}
// Select the latest matching version
const release = releases.releases
.filter(info => versionRange.testVersion(info.version))
.reduce<ModPortalReleaseType | undefined>((max, cur) => (
max && lib.integerFullVersion(max.version) > lib.integerFullVersion(cur.version) ? max : cur
), undefined);
if (!release) {
return null;
}
// Create the mod info class from the selected
try {
return lib.ModInfo.fromJSON({
...release.info_json, // includes dependencies
sha1: release.sha1,
version: release.version,
name: releases.name,
author: releases.owner,
title: releases.title,
});
} catch {
// Can error if there are invalid dependencies
errors.set(releases.name, "invalidDependency");
return null;
}
}
while (searchQueue.length > 0) {
// Dequeue the next mod, skip if incompatible
const mod = searchQueue.shift()!;
if (mod.incompatible) {
incompatible.add(mod.name);
errors.set(mod.name, "incompatible");
continue;
}
@@ -674,13 +707,13 @@ export default class ControlConnection extends BaseConnection {
versionRange.combineVersion(mod.version);
if (!versionRange.valid) {
candidateReleases.delete(mod.name);
invalid.add(mod.name);
errors.set(mod.name, "unsatisfiable");
}
}
// Do not add dependencies for invalids, optionals, skips, or valid candidates
// Do not add dependencies for errors, skips, optionals, or valid candidates
const existingCandidate = candidateReleases.get(mod.name);
if (invalid.has(mod.name) || optionalRequirements.has(mod.name) || skipSearch.has(mod.name) || (
if (errors.has(mod.name) || skipSearch.has(mod.name) || optionalRequirements.has(mod.name) || (
existingCandidate && versionRange.testVersion(existingCandidate.version)
)) {
continue;
@@ -695,35 +728,11 @@ export default class ControlConnection extends BaseConnection {
// Check if a mod poral version fits the requirements
if (!candidate || checkForUpdates) {
const modReleases = await fetchModReleases(mod.name);
if (modReleases && modReleases.releases) {
const release = modReleases.releases
.filter(info => (
info.info_json.factorio_version === factorioVersion
&& versionRange.testVersion(info.version)
))
.reduce<ModPortalReleaseType | undefined>((max, cur) => (
max && lib.integerFullVersion(max.version) > lib.integerFullVersion(cur.version) ? max : cur
), undefined);
// We only select the mod portal release if it is more recent than the local candidate
if (release && (!candidate || lib.integerFullVersion(release.version) > candidate.integerVersion)) {
// eslint-disable-next-line max-depth
try {
candidate = lib.ModInfo.fromJSON({
...release.info_json, // Also includes dependencies
sha1: release.sha1,
version: release.version,
name: modReleases.name,
author: modReleases.owner,
title: modReleases.title,
});
} catch {
// Can error with invalid dependencies, in which case we highlight it as invalid
// We modify the name to expose the error to the user.
invalid.add(`${modReleases.name} (Has invalid dependency)`);
}
}
const releases = await fetchModReleases(mod.name);
const release = selectModRelease(releases, versionRange);
// We only select the mod portal release if it is more recent than the local candidate
if (release && (!candidate || release.integerVersion > candidate.integerVersion)) {
candidate = release;
}
}
@@ -739,14 +748,22 @@ export default class ControlConnection extends BaseConnection {
}
}
// Add the not found dependencies as errors
for (const modName of dependencyRequirements.keys()) {
if (!candidateReleases.has(modName) && !errors.has(modName)) {
errors.set(modName, "notFound");
}
}
// Remove incompatible dependencies if they aren't required by others
for (const [modName, error] of errors) {
if (error === "incompatible" && !dependencyRequirements.has(modName)) {
errors.delete(modName);
}
}
return new lib.ModDependencyResolveRequest.Response(
[...candidateReleases.values()],
[...invalid.values(), ...[...incompatible.values()].filter(mod => (
candidateReleases.has(mod) || builtInModNames.has(mod)
))],
[...dependencyRequirements.keys()].filter(mod => (
!candidateReleases.has(mod) && !incompatible.has(mod) && !invalid.has(mod)
)),
[...candidateReleases.values()], errors,
);
}
+17 -5
View File
@@ -403,6 +403,10 @@ export class ModPortalDownloadRequest {
}
}
const ModDependencyResolveErrors = ["incompatible", "unsatisfiable", "invalidDependency", "notFound"] as const;
export const ModDependencyResolveErrorsSchema = Type.Union(ModDependencyResolveErrors.map(v => Type.Literal(v)));
export type ModDependencyResolveErrors = Static<typeof ModDependencyResolveErrorsSchema>;
/**
* Request mods dependencies to be resolved using the Factorio mod portal.
*
@@ -456,18 +460,26 @@ export class ModDependencyResolveRequest {
static Response = class ModDependencyResolveResponse {
constructor(
public dependencies: ModInfo[],
public incompatible: string[],
public missing: string[],
public errors: Map<string, ModDependencyResolveErrors>,
) {}
static jsonSchema = Type.Object({
"dependencies": Type.Array(ModInfo.jsonSchema),
"incompatible": Type.Array(Type.String()),
"missing": Type.Array(Type.String()),
"errors": Type.Record(Type.String(), ModDependencyResolveErrorsSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.dependencies.map(d => ModInfo.fromJSON(d)), json.incompatible, json.missing);
return new this(
json.dependencies.map(d => ModInfo.fromJSON(d)),
new Map(Object.entries(json.errors))
);
}
toJSON(): Static<typeof ModDependencyResolveResponse.jsonSchema> {
return {
dependencies: this.dependencies.map(d => d.toJSON()),
errors: Object.fromEntries(this.errors),
};
}
};
}
@@ -268,16 +268,28 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
// State for the modal and dependencies
const [open, setOpen] = useState(false);
const [missing, setMissing] = useState<string[]>([]);
const [builtins, setBuiltins] = useState<string[]>([]);
const [incompatible, setIncompatible] = useState<string[]>([]);
const [factorioVersion, setFactorioVersion] = useState<lib.ApiVersion | null>(null);
// State for all mods fetched from backend
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [allMods, setAllMods] = useState<lib.ModInfo[]>([]);
const [mods, setMods] = useState<lib.ModInfo[]>([]);
const [factorioVersion, setFactorioVersion] = useState<lib.ApiVersion | null>(null);
const [mods, setMods] = useState<lib.ModInfo[]>([]); // All resolved mods and dependencies
const [missing, setMissing] = useState<lib.ModInfo[]>([]); // Missing mods and dependencies, excluding builtin
const [modErrors, setModErrors] = useState({
notFound: new Array<string>(), // Mods which were not found on the mod portal, can include builtins
incompatible: new Array<string>(), // Mods which are required, but are marked incompatible by another
unsatisfiable: new Array<string>(), // Mods which can not have their version range satisfied
invalidDependency: new Array<string>(), // Mods with invalid dependency specs
});
function resetModErrors() {
setModErrors({
notFound: [],
incompatible: [],
unsatisfiable: [],
invalidDependency: [],
});
}
// Get a valid factorio version
useEffect(() => {
@@ -294,10 +306,8 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
if (!open || !factorioVersion) {
// Clear if closed or no version
setMods([]);
setAllMods([]);
setMissing([]);
setBuiltins([]);
setIncompatible([]);
resetModErrors();
return;
}
@@ -309,21 +319,29 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
lib.ModDependencyResolveRequest.fromModPackEnabled(props.modPack, props.checkForUpdates)
).then(response => {
if (canceled) { return; }
setMods(response.dependencies.filter(dep => !modInfos.has(`${dep.name}_${dep.version}`)));
setIncompatible(response.incompatible);
setAllMods(response.dependencies);
setMissing(response.missing.filter(name => !props.builtInModNames.includes(name)));
setBuiltins(response.missing.filter(name => props.builtInModNames.includes(name)));
const newModErrors = [...response.errors.entries()]
.reduce<typeof modErrors>((acc, [modName, modError]) => {
acc[modError].push(modName);
return acc;
}, {
notFound: [],
incompatible: [],
unsatisfiable: [],
invalidDependency: [],
});
setModErrors(newModErrors);
setMods(response.dependencies);
setMissing(response.dependencies.filter(dep => !modInfos.has(`${dep.name}_${dep.version}`)));
setLoading(false);
}).catch(err => {
if (canceled) { return; }
notifyErrorHandler("Error fetching mods dependencies")(err);
setError(err);
setMods([]);
setAllMods([]);
setMissing([]);
setBuiltins([]);
setIncompatible([]);
resetModErrors();
setLoading(false);
});
@@ -334,51 +352,56 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
// Re-fetch when modal opens or factorio version changes
}, [open, factorioVersion, control]);
// Function to handle download from Mod Portal to Controller
function handleControllerDownload() {
if (!factorioVersion || !mods) {
// Fix any mod errors found after resolving dependencies
function fixModErrors() {
if (!factorioVersion) {
return;
}
// Enable any required builtins
for (const name of builtins) {
props.onChange({
type: "mods.set",
name: name,
value: { ...props.modPack.mods.get(name)!, enabled: true },
});
// Enable any required builtins, they will be within the not found list
for (const modName of modErrors.notFound) {
if (props.builtInModNames.includes(modName)) {
props.onChange({
type: "mods.set",
name: modName,
value: { ...props.modPack.mods.get(modName)!, enabled: true },
});
}
}
// Disable any incompatible mods present
for (const name of incompatible) {
const modRecord = props.modPack.mods.get(name);
// Disable any incompatible mods
for (const modName of modErrors.incompatible) {
const modRecord = props.modPack.mods.get(modName);
if (modRecord) {
props.onChange({
type: "mods.set",
name: name,
name: modName,
value: { ...modRecord, enabled: false },
});
}
}
// Add all dependencies to the mod pack
for (const mod of allMods) {
props.onChange({
type: "mods.set",
name: mod.name,
value: {
for (const mod of mods) {
const existingMod = props.modPack.mods.get(mod.name);
if (!existingMod || existingMod.version !== mod.version) {
props.onChange({
type: "mods.set",
name: mod.name,
version: mod.version,
sha1: mod.sha1,
enabled: true,
},
});
value: {
name: mod.name,
version: mod.version,
sha1: mod.sha1,
enabled: true,
},
});
}
}
// Download the missing dependencies
// Download any missing mods
control.send(
new lib.ModPortalDownloadRequest(
mods.map(mod => ({
missing.map(mod => ({
name: mod.name, version: new lib.ModVersionEquality("=", mod.version),
})),
factorioVersion,
@@ -386,12 +409,12 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
).then(() => {
notification.success({
message: "Download complete",
description: `${mods.length} mods have been downloaded to the controller.`,
description: `${missing.length} mods have been downloaded to the controller.`,
});
}).catch(
notifyErrorHandler("Error starting mod download")
);
};
}
return <>
{props.checkForUpdates && <Button
@@ -415,8 +438,11 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
<Button
key="start"
type="primary"
disabled={loading || mods.length === 0}
onClick={() => { setOpen(false); handleControllerDownload(); }}
disabled={loading || missing.length === 0}
onClick={() => {
setOpen(false);
fixModErrors();
}}
>Start Download</Button>,
]}
>
@@ -429,30 +455,26 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) {
style={{ marginBottom: 16 }}
/>}
{/* Display warning message if dependencies could not be found */}
{!loading && missing.length > 0 && <Alert
message="Failed to Find Dependencies:"
description={<ul>
{missing.map(name => <li>{name}</li>)}
</ul>}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>}
{/* Display warning message if dependencies are incompatible */}
{!loading && incompatible.length > 0 && <Alert
message="Incompatible Dependencies Found:"
description={<ul>
{incompatible.map(name => <li>{name}</li>)}
</ul>}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>}
{/* Display warning message for mod errors */}
{([
["notFound", "Failed to find dependencies:"],
["incompatible", "Dependencies are incompatible:"],
["unsatisfiable", "Dependencies could not be satisfied:"],
["invalidDependency", "Dependencies has invalid specifications:"],
] as const).map(([prop, message]) => {
const modNames = modErrors[prop];
return modNames.length === 0 ? null : <Alert
showIcon
type="warning"
message={message}
description={<ul>{modNames.map(name => <li>{name}</li>)}</ul>}
style={{ marginBottom: 14 }}
/>;
})}
{/* Display a table of the missing mods that need to be downloaded */}
<Table
dataSource={mods}
dataSource={missing}
rowKey={record => record.name}
loading={loading}
pagination={{
+60 -88
View File
@@ -168,19 +168,21 @@ describe("messages/mod", function() {
ModInfo.fromJSON({ name: "bar", version: "2.0.0" }),
],
],
[[], ["foo-i"], ["foo-i", "bar-i"]], // incompatible
[[], ["foo-m"], ["foo-m", "bar-m"]], // missing
[ // errors
new Map([]),
new Map([["foo", "notFound"]]),
new Map([["foo", "unsatisfiable"], ["bar", "incompatible"], ["baz", "invalidDependency"]]),
],
);
it("should be round trip json serialisable", function() {
testRoundTripJsonSerialisable(ModDependencyResolveRequest.Response, tests);
});
it("should be construable", function() {
for (const [dependencies, incompatible, missing] of tests) {
const test = JSON.stringify([dependencies, missing, incompatible]);
const request = new ModDependencyResolveRequest.Response(dependencies, incompatible, missing);
for (const [dependencies, errors] of tests) {
const test = JSON.stringify([dependencies, errors]);
const request = new ModDependencyResolveRequest.Response(dependencies, errors);
assert.deepEqual(request.dependencies, dependencies, test);
assert.deepEqual(request.incompatible, incompatible, test);
assert.deepEqual(request.missing, missing, test);
assert.deepEqual(request.errors, errors, test);
}
});
});
@@ -204,19 +206,15 @@ describe("messages/mod", function() {
)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, []);
const expectedIds = [
"root_1.0.0", "foo_1.0.0", "foo-dep_1.0.0", "bar_1.0.0",
"root-2_1.0.0", "bar-2_1.0.0",
];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set([
"root_1.0.0", "foo_1.0.0", "foo-dep_1.0.0", "bar_1.0.0",
"root-2_1.0.0", "bar-2_1.0.0",
]));
}
});
it("highlights dependencies with no suitable version", async function() {
@@ -228,8 +226,10 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base", "bar"]);
assert.deepEqual(result.incompatible, []);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["bar", "notFound"],
]));
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0].id, "root_1.0.0");
@@ -243,8 +243,10 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base", "foo"]);
assert.deepEqual(result.incompatible, []);
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");
@@ -260,16 +262,13 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, ["foo-dep"]);
const expectedIds = ["root_1.0.0", "foo_1.0.0", "foo-dep_1.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["foo-dep", "incompatible"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("highlights dependencies with conflicting versions", async function() {
@@ -282,16 +281,13 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, ["dep"]);
const expectedIds = ["root_1.0.0", "foo_1.0.0", "bar_1.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
["dep", "unsatisfiable"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0", "bar_1.0.0"]));
}
});
it("highlights incompatible builtin mods", async function() {
@@ -303,16 +299,12 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, []);
assert.deepEqual(result.incompatible, ["base"]);
const expectedIds = ["root_1.0.0", "foo_1.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "incompatible"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("does not ignore optional dependencies when required by another", async function() {
@@ -325,16 +317,12 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, []);
const expectedIds = ["root_1.0.0", "foo_1.0.0", "baz_1.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0", "baz_1.0.0"]));
}
});
it("prefers the locally installed version if matching", async function() {
@@ -347,16 +335,12 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, []);
const expectedIds = ["root_1.0.0", "foo_1.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_1.0.0"]));
}
});
it("prefers the mod portal version when checking for updates", async function() {
@@ -369,16 +353,12 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion, true)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, []);
const expectedIds = ["root_1.0.0", "foo_2.0.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0"]));
}
});
it("prefers the latest mod portal version when using the mod portal", async function() {
@@ -409,16 +389,12 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("root")], factorioVersion)
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, []);
const expectedIds = ["root_1.0.0", "foo_2.0.0", "bar_1.5.0"];
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const depIds = new Set(result.dependencies.map(mod => mod.id));
assert.equal(depIds.size, expectedIds.length);
for (const modId of expectedIds) {
assert(depIds.has(modId), `Missing ${modId}`);
}
assert.deepEqual(depIds, new Set(["root_1.0.0", "foo_2.0.0", "bar_1.5.0"]));
}
});
it("rejects when a network error occurs", async function() {
@@ -440,22 +416,18 @@ describe("messages/mod", function() {
new lib.ModDependencyResolveRequest([new ModDependency("pymodpack = 3.0.0")], "2.0")
);
assert.deepEqual(result.missing, ["base"]);
assert.deepEqual(result.incompatible, ["space-age"]);
assert.deepEqual(result.errors, new Map([
["base", "notFound"],
]));
const expectedNames = [ // Can't use ids as the versions may change
const depNames = new Set(result.dependencies.map(mod => mod.name));
assert.deepEqual(depNames, new Set([ // Can't use ids as the versions may change
"pymodpack", "pypostprocessing", "pyalternativeenergy", "pyalternativeenergygraphics",
"pyalienlife", "pyalienlifegraphics", "pyalienlifegraphics2", "pyalienlifegraphics3",
"pycoalprocessing", "pycoalprocessinggraphics", "pyfusionenergy", "pyfusionenergygraphics",
"pypetroleumhandling", "pypetroleumhandlinggraphics", "pyrawores", "pyraworesgraphics",
"pyhightech", "pyhightechgraphics", "pyindustry", "pyindustrygraphics",
];
const depNames = new Set(result.dependencies.map(mod => mod.name));
assert.equal(depNames.size, expectedNames.length);
for (const modName of expectedNames) {
assert(depNames.has(modName), `Missing ${modName}`);
}
]));
});
});
});