diff --git a/CHANGELOG.md b/CHANGELOG.md
index a76cc7a1..09988b31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1122: Packaging should recognize resources in dependency modules set to deploy
- #1119: The update command should check version requirements using post-update values instead of what's currently installed
- #1097: The Test resource processor now supports nested tests
+- #1116: Fix behavior inconsistencies between install and uninstall for package name casing.
### Security
- requests Python wheel updated to 2.33.0
diff --git a/src/cls/IPM/Storage/ResourceReference.cls b/src/cls/IPM/Storage/ResourceReference.cls
index 0a7d977e..4831adb2 100644
--- a/src/cls/IPM/Storage/ResourceReference.cls
+++ b/src/cls/IPM/Storage/ResourceReference.cls
@@ -125,13 +125,15 @@ ClassMethod GetChildren(
set tResourceCache($$$lcase(tCacheResult.%Get("UniqueName"))) = ""
}
}
- // include hidden files by temporarily enable showHidden global for Studio search
+ // Include hidden files by temporarily enabling showHidden global for Studio search.
+ // Use %SQLUPPER for case-insensitive package prefix matching instead of relying on
+ // StudioOpenDialog's built-in prefix filter, which is case-sensitive.
set tHidden = $get(^%SYS("Studio", "ShowHidden"), $char(0))
set ^%SYS("Studio","ShowHidden") = 1
- // PKG extension should only cover .CLS files
set tFilesResult = ##class(%SQL.Statement).%ExecDirect(,
- "select Name from %Library.RoutineMgr_StudioOpenDialog(?,'',1,1,1,0,0)",
- tPackage_"*.cls,"_tPackage_"*.mac,"_tPackage_"*.int,"_tPackage_"*.inc")
+ "select Name from %Library.RoutineMgr_StudioOpenDialog(?,'',1,1,1,0,0) "
+ _"where %SQLUPPER(Name) %STARTSWITH %SQLUPPER(?)",
+ "*.cls,*.mac,*.int,*.inc", tPackage)
if (tFilesResult.%SQLCODE < 0) {
set tSC = $$$ERROR($$$SQLCode,tFilesResult.%SQLCODE,tFilesResult.%Message)
quit
diff --git a/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls b/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls
new file mode 100644
index 00000000..939da212
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls
@@ -0,0 +1,50 @@
+/// Tests that uninstall correctly handles PKG resources where the casing of the package
+/// name in module.xml differs from the casing of the stored class/routine/include names,
+/// and that resources owned by other modules within the same package are not deleted.
+Class Test.PM.Integration.PkgCaseUninstall Extends Test.PM.Integration.Base
+{
+
+/// module.xml declares Test.PKGCASE.PKG (uppercase) but actual resources are Test.PkgCase.*
+Parameter ModuleName = "pkg-case-test";
+
+Method TestPkgCaseMismatchUninstall()
+{
+ set moduleDir = ..GetModuleDir(..#ModuleName)
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDir), "Loaded pkg-case-test")
+
+ // Confirm resources were loaded
+ do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.MyClass"), "Class exists after load")
+ do $$$AssertTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyRoutine.MAC"), "Routine exists after load")
+ do $$$AssertTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyInclude.INC"), "Include exists after load")
+
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall " _ ..#ModuleName), "Uninstalled pkg-case-test")
+
+ // All three resource types must be removed despite the casing mismatch in module.xml
+ do $$$AssertNotTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.MyClass"), "Class removed after uninstall")
+ do $$$AssertNotTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyRoutine.MAC"), "Routine removed after uninstall")
+ do $$$AssertNotTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyInclude.INC"), "Include removed after uninstall")
+}
+
+/// Verifies that uninstalling a PKG module with a casing mismatch does not delete resources
+/// owned by a different module that happen to fall within the same package prefix.
+Method TestPkgCaseOwnershipExclusion()
+{
+ set moduleDirA = ..GetModuleDir(..#ModuleName)
+ set moduleDirB = ..GetModuleDir("pkg-case-extra")
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDirA), "Loaded pkg-case-test")
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDirB), "Loaded pkg-case-extra")
+
+ do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass exists after load")
+
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall " _ ..#ModuleName), "Uninstalled pkg-case-test")
+
+ // ExtraClass belongs to pkg-case-extra and must survive uninstall of pkg-case-test
+ do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass not removed by uninstall of pkg-case-test")
+
+ do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall pkg-case-extra"), "Uninstalled pkg-case-extra")
+
+ // ExtraClass should now be removed after uninstalling its owning module
+ do $$$AssertNotTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass removed by uninstall of pkg-case-extra")
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls
new file mode 100644
index 00000000..17145677
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls
@@ -0,0 +1,4 @@
+Class Test.PkgCase.ExtraClass
+{
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml
new file mode 100644
index 00000000..628ebc03
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml
@@ -0,0 +1,10 @@
+
+
+
+ pkg-case-extra
+ 0.0.1
+
+
+
+
+
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls
new file mode 100644
index 00000000..2f1d3097
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls
@@ -0,0 +1,9 @@
+Class Test.PkgCase.MyClass
+{
+
+ClassMethod Hello() As %String
+{
+ quit "hello"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc
new file mode 100644
index 00000000..2514d5b2
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc
@@ -0,0 +1,4 @@
+^INC^Save for Source Control^^~Format=Cache.S~^UTF8
+%RO
+Test.PkgCase.MyInclude^INC^^^0
+#define PkgCaseValue 1
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac
new file mode 100644
index 00000000..bd2430fb
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac
@@ -0,0 +1,2 @@
+ROUTINE Test.PkgCase.MyRoutine
+ write !, "Hello from Test.PkgCase.MyRoutine"
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml
new file mode 100644
index 00000000..d5bf62e5
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml
@@ -0,0 +1,10 @@
+
+
+
+ pkg-case-test
+ 0.0.1
+
+
+
+
+