From cfbfb03cb1d98593af038453405a35e9bc249b2a Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 10:37:06 -0700 Subject: [PATCH 1/2] fix(ModuleLoader): an unparseable unmanaged module version failing the load A required module that returns a plain version value instead of a DepCtrl record is wrapped in an unmanaged record before its version is checked. When that value doesn't parse, the record constructor asserts, so the requiring script fails with "Bad DependencyControl record" and the module is never offered to the updater. Nothing short of deleting the file recovers from it. This shows up with Aegisub-Motion's a-mo.Log, whose v1.1.0 release zip shipped with an untemplated "##__LOG_VERSION__##" version. a-mo.Log has been 1.0.0 since 2015, so no version bump ever replaces a broken copy. Treat a version that doesn't parse as 0.0.0. It fails any real requirement, so the module goes through the regular outdated-module update path. If that update fails, the outdated message now shows the installed value verbatim instead of "vnil". Co-Authored-By: Claude Opus 5.5 --- DependencyControl.json | 3 +- .../l0/DependencyControl/ModuleLoader.moon | 7 ++-- .../DependencyControl/test/ModuleLoader.moon | 35 +++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/DependencyControl.json b/DependencyControl.json index 8b5ec96..97122d1 100644 --- a/DependencyControl.json +++ b/DependencyControl.json @@ -802,7 +802,8 @@ "feat(CLI): `--serve-channel` can be given more than once to graft the dev build onto several channels at once, the first of which becomes the served feed's default.", "fix: A package's settings are no longer reverted by a second DependencyControl instance writing the same config file (e.g. during a DependencyControl update, in another automation script context, or from another Aegisub process). Each save now writes only the fields it changed since it last read the file.", "fix: A regression introduced in v0.7.0 was fixed that prevented the new version number of updated packages from being written into the DependencyControl config file when nothing else in its DependencyControl record changed.", - "fix: Changes to settings that affect feed fetching (private host blocking, feed size and timeout limits) now take effect immediately instead of requiring an automation scripts reload." + "fix: Changes to settings that affect feed fetching (private host blocking, feed size and timeout limits) now take effect immediately instead of requiring an automation scripts reload.", + "fix: A required module that isn't managed by DependencyControl and declares a version that can't be parsed no longer fails to load with a 'Bad DependencyControl record' error. It is now treated as outdated and updated from its feed." ] } }, diff --git a/modules/l0/DependencyControl/ModuleLoader.moon b/modules/l0/DependencyControl/ModuleLoader.moon index febf2a5..7f9244a 100644 --- a/modules/l0/DependencyControl/ModuleLoader.moon +++ b/modules/l0/DependencyControl/ModuleLoader.moon @@ -45,7 +45,8 @@ class ModuleLoader url = url and ": #{url}" or "" if ref -- unmanaged records have refs whose .version is a string instead of a DepCtrl record - version = SemanticVersion\toString type(ref.version) == "table" and ref.version.version or ref.version + rawVersion = type(ref.version) == "table" and ref.version.version or ref.version + version = SemanticVersion\toString(rawVersion) or tostring rawVersion return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason else reqVersion = reqVersion and " (v#{reqVersion})" or "" @@ -162,7 +163,9 @@ class ModuleLoader continue if not ModuleProvider.isDepCtrlVersionRecord record - record = @@ moduleName: .moduleName, version: record, recordType: domain.RecordType.Unmanaged + -- a version that doesn't parse can't satisfy the requirement, so it counts as 0.0.0 and the module gets updated + version = SemanticVersion\toPacked(record) or 0 + record = @@ moduleName: .moduleName, :version, recordType: domain.RecordType.Unmanaged -- force an update for outdated modules if not record\checkVersion .version diff --git a/modules/l0/DependencyControl/test/ModuleLoader.moon b/modules/l0/DependencyControl/test/ModuleLoader.moon index 7e51819..34e132b 100644 --- a/modules/l0/DependencyControl/test/ModuleLoader.moon +++ b/modules/l0/DependencyControl/test/ModuleLoader.moon @@ -36,6 +36,12 @@ ut\assertContains result, "Required: v2.0.0" ut\assertContains result, "1.1.1" + formatVersionErrorTemplate_outdated_unparseableRef: (ut) -> + ref = {version: "##__LOG_VERSION__##"} + result = ModuleLoader.formatVersionErrorTemplate nil, "MyModule", "1.0.0", nil, "too old", ref + ut\assertContains result, "Installed: v##__LOG_VERSION__##" + ut\assertContains result, "Required: v1.0.0" + formatVersionErrorTemplate_outdated_tableRef: (ut) -> ref = {version: {version: 65793}} -- 1*65536 + 1*256 + 1 = "1.1.1" in base-256 encoding result = ModuleLoader.formatVersionErrorTemplate nil, "MyModule", "2.0.0", nil, "too old", ref @@ -281,6 +287,29 @@ ut\assertEquals err, "" ut\assertEquals mdl._ref, newRef + -- loadModules: an unmanaged module whose version string doesn't parse gets an unmanaged record at + -- 0.0.0, which fails the requirement and sends the module to the updater. + loadModules_unparseableUnmanagedVersionForcesUpdate: (ut) -> + ns = "test.ModuleLoader.unparseableVersion" + newRef = {updated: true} + loadedRef = {version: "##__LOG_VERSION__##"} + local constructedArgs, requiredRecord + updater = {require: ((record) => requiredRecord = record; newRef)} + recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater}, + {__call: (cls, args) -> + constructedArgs = args + {checkVersion: ((target) => SemanticVersion\toPacked(args.version) >= SemanticVersion\toPacked target)}} + rec = {feed: nil, moduleName: "host.Module", name: "host", __class: recClass} + mdl = {moduleName: ns, name: ns, version: SemanticVersion\toPacked "1.0.0"} + (ut\stub ModuleLoader, "loadModule")\calls (self, m, usePrivate) -> m._ref = loadedRef unless usePrivate + success, err = ModuleLoader.loadModules rec, {mdl} + ut\assertTrue success + ut\assertEquals err, "" + ut\assertEquals constructedArgs.version, 0 + ut\assertEquals constructedArgs.recordType, domain.RecordType.Unmanaged + ut\assertNotNil requiredRecord + ut\assertEquals mdl._ref, newRef + -- loadModules: an outdated *required* module the updater can't update fails with an "outdated" error loadModules_outdatedRequiredFails: (ut) -> ns = "test.ModuleLoader.outdatedFail" @@ -344,7 +373,8 @@ _order: { "formatVersionErrorTemplate_missing_bare", "formatVersionErrorTemplate_missing_withVersion", "formatVersionErrorTemplate_missing_withUrl", - "formatVersionErrorTemplate_outdated_scalarRef", "formatVersionErrorTemplate_outdated_tableRef", + "formatVersionErrorTemplate_outdated_scalarRef", "formatVersionErrorTemplate_outdated_unparseableRef", + "formatVersionErrorTemplate_outdated_tableRef", "createDummyRef_nonModule", "createDummyRef_newRef", "createDummyRef_existingRef", "removeDummyRef_nonModule", "removeDummyRef_dummy", "removeDummyRef_nonDummy", "loadModule_cached", "loadModule_success", "loadModule_missing", "loadModule_error", @@ -352,7 +382,8 @@ "loadModules_skipsModule", "loadModules_allLoaded", "loadModules_missingFetchedViaUpdater", "loadModules_missingRequiredFails", "loadModules_missingOptionalSkipped", "loadModules_requirementsUnmetSurfacesNestedReason", - "loadModules_outdatedForcesUpdate", "loadModules_outdatedRequiredFails", + "loadModules_outdatedForcesUpdate", "loadModules_unparseableUnmanagedVersionForcesUpdate", + "loadModules_outdatedRequiredFails", "loadModules_updateInProgressAcceptsLoadedRef", "checkOptionalModules_noneOptional", "checkOptionalModules_missingOptional" } From a078080c14ed3350ccb74dad98b8538bc370f928 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Wed, 7 Oct 2026 10:43:04 -0700 Subject: [PATCH 2/2] test(ModuleLoader): return the fetched ref from the unparseable-version updater stub Co-Authored-By: Claude Opus 5.5 --- modules/l0/DependencyControl/test/ModuleLoader.moon | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/l0/DependencyControl/test/ModuleLoader.moon b/modules/l0/DependencyControl/test/ModuleLoader.moon index 34e132b..dc892ec 100644 --- a/modules/l0/DependencyControl/test/ModuleLoader.moon +++ b/modules/l0/DependencyControl/test/ModuleLoader.moon @@ -294,7 +294,9 @@ newRef = {updated: true} loadedRef = {version: "##__LOG_VERSION__##"} local constructedArgs, requiredRecord - updater = {require: ((record) => requiredRecord = record; newRef)} + updater = {require: (record) => + requiredRecord = record + newRef} recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater}, {__call: (cls, args) -> constructedArgs = args