@@ -117,6 +117,226 @@ class PluginHotReloadServiceTest : BasePlatformTestCase() {
117117 assertNull(" Should return null when id element is missing" , pluginId)
118118 }
119119
120+ /* *
121+ * Tests extraction of plugin ID from IntelliJ's actual plugin zip structure:
122+ * plugin-name/lib/plugin-name.jar containing META-INF/plugin.xml
123+ *
124+ * This is the format produced by `./gradlew buildPlugin`.
125+ */
126+ fun testExtractPluginIdFromJarInsideZip () {
127+ val pluginXml = """
128+ <idea-plugin>
129+ <id>com.example.jar-nested-plugin</id>
130+ <name>Jar Nested Plugin</name>
131+ </idea-plugin>
132+ """ .trimIndent()
133+
134+ // Create the inner JAR with META-INF/plugin.xml
135+ val innerJarBytes = ByteArrayOutputStream ().use { jarBaos ->
136+ ZipOutputStream (jarBaos).use { jarZos ->
137+ jarZos.putNextEntry(ZipEntry (" META-INF/" ))
138+ jarZos.closeEntry()
139+ jarZos.putNextEntry(ZipEntry (" META-INF/plugin.xml" ))
140+ jarZos.write(pluginXml.toByteArray(StandardCharsets .UTF_8 ))
141+ jarZos.closeEntry()
142+ }
143+ jarBaos.toByteArray()
144+ }
145+
146+ // Create the outer ZIP with the plugin structure
147+ val zipBytes = ByteArrayOutputStream ().use { baos ->
148+ ZipOutputStream (baos).use { zos ->
149+ zos.putNextEntry(ZipEntry (" jar-nested-plugin/" ))
150+ zos.closeEntry()
151+ zos.putNextEntry(ZipEntry (" jar-nested-plugin/lib/" ))
152+ zos.closeEntry()
153+ zos.putNextEntry(ZipEntry (" jar-nested-plugin/lib/jar-nested-plugin.jar" ))
154+ zos.write(innerJarBytes)
155+ zos.closeEntry()
156+ }
157+ baos.toByteArray()
158+ }
159+
160+ val service = PluginHotReloadService ()
161+ val pluginId = service.extractPluginId(zipBytes)
162+
163+ assertEquals(" com.example.jar-nested-plugin" , pluginId)
164+ }
165+
166+ /* *
167+ * Tests that we can extract plugin ID when there are multiple jars,
168+ * and the plugin.xml is in the second jar (not the first).
169+ */
170+ fun testExtractPluginIdFromSecondJarInZip () {
171+ val pluginXml = """
172+ <idea-plugin>
173+ <id>com.example.multi-jar-plugin</id>
174+ <name>Multi Jar Plugin</name>
175+ </idea-plugin>
176+ """ .trimIndent()
177+
178+ // Create a jar WITHOUT plugin.xml (like a dependency)
179+ val depJarBytes = ByteArrayOutputStream ().use { jarBaos ->
180+ ZipOutputStream (jarBaos).use { jarZos ->
181+ jarZos.putNextEntry(ZipEntry (" com/example/Dep.class" ))
182+ jarZos.write(" fake class bytes" .toByteArray())
183+ jarZos.closeEntry()
184+ }
185+ jarBaos.toByteArray()
186+ }
187+
188+ // Create the plugin jar WITH plugin.xml
189+ val pluginJarBytes = ByteArrayOutputStream ().use { jarBaos ->
190+ ZipOutputStream (jarBaos).use { jarZos ->
191+ jarZos.putNextEntry(ZipEntry (" META-INF/" ))
192+ jarZos.closeEntry()
193+ jarZos.putNextEntry(ZipEntry (" META-INF/plugin.xml" ))
194+ jarZos.write(pluginXml.toByteArray(StandardCharsets .UTF_8 ))
195+ jarZos.closeEntry()
196+ }
197+ jarBaos.toByteArray()
198+ }
199+
200+ // Create the outer ZIP with multiple jars
201+ val zipBytes = ByteArrayOutputStream ().use { baos ->
202+ ZipOutputStream (baos).use { zos ->
203+ zos.putNextEntry(ZipEntry (" multi-jar-plugin/" ))
204+ zos.closeEntry()
205+ zos.putNextEntry(ZipEntry (" multi-jar-plugin/lib/" ))
206+ zos.closeEntry()
207+ // Add dependency jar first (no plugin.xml)
208+ zos.putNextEntry(ZipEntry (" multi-jar-plugin/lib/dependency.jar" ))
209+ zos.write(depJarBytes)
210+ zos.closeEntry()
211+ // Add plugin jar second (has plugin.xml)
212+ zos.putNextEntry(ZipEntry (" multi-jar-plugin/lib/plugin.jar" ))
213+ zos.write(pluginJarBytes)
214+ zos.closeEntry()
215+ }
216+ baos.toByteArray()
217+ }
218+
219+ val service = PluginHotReloadService ()
220+ val pluginId = service.extractPluginId(zipBytes)
221+
222+ assertEquals(" com.example.multi-jar-plugin" , pluginId)
223+ }
224+
225+ /* *
226+ * Tests extraction with the actual mcp-steroids plugin zip structure simulation.
227+ * This mimics: intellij-mcp-steroid/lib/intellij-mcp-steroid-VERSION.jar
228+ */
229+ fun testExtractPluginIdLikeMcpSteroids () {
230+ val pluginXml = """
231+ <idea-plugin>
232+ <id>com.jonnyzzz.intellij.mcp-steroid</id>
233+ <name>IntelliJ MCP Steroid</name>
234+ <vendor>jonnyzzz.com</vendor>
235+ </idea-plugin>
236+ """ .trimIndent()
237+
238+ // Create the plugin jar
239+ val pluginJarBytes = ByteArrayOutputStream ().use { jarBaos ->
240+ ZipOutputStream (jarBaos).use { jarZos ->
241+ jarZos.putNextEntry(ZipEntry (" META-INF/" ))
242+ jarZos.closeEntry()
243+ jarZos.putNextEntry(ZipEntry (" META-INF/plugin.xml" ))
244+ jarZos.write(pluginXml.toByteArray(StandardCharsets .UTF_8 ))
245+ jarZos.closeEntry()
246+ // Add some fake class files like a real plugin would have
247+ jarZos.putNextEntry(ZipEntry (" com/jonnyzzz/intellij/mcp/SteroidsMcpServer.class" ))
248+ jarZos.write(" fake class" .toByteArray())
249+ jarZos.closeEntry()
250+ }
251+ jarBaos.toByteArray()
252+ }
253+
254+ // Create the outer ZIP with the exact structure buildPlugin produces
255+ val zipBytes = ByteArrayOutputStream ().use { baos ->
256+ ZipOutputStream (baos).use { zos ->
257+ zos.putNextEntry(ZipEntry (" intellij-mcp-steroid/" ))
258+ zos.closeEntry()
259+ zos.putNextEntry(ZipEntry (" intellij-mcp-steroid/lib/" ))
260+ zos.closeEntry()
261+ zos.putNextEntry(ZipEntry (" intellij-mcp-steroid/lib/intellij-mcp-steroid-0.84.0-SNAPSHOT.jar" ))
262+ zos.write(pluginJarBytes)
263+ zos.closeEntry()
264+ }
265+ baos.toByteArray()
266+ }
267+
268+ val service = PluginHotReloadService ()
269+ val pluginId = service.extractPluginId(zipBytes)
270+
271+ assertEquals(" com.jonnyzzz.intellij.mcp-steroid" , pluginId)
272+ }
273+
274+ /* *
275+ * Tests that attempting to reload the hot-reload plugin itself is blocked.
276+ * The plugin cannot reload itself - it would unload mid-execution.
277+ */
278+ fun testSelfReloadIsBlocked () {
279+ val pluginXml = """
280+ <idea-plugin>
281+ <id>com.jonnyzzz.intellij.hot-reload</id>
282+ <name>Hot Reload</name>
283+ </idea-plugin>
284+ """ .trimIndent()
285+
286+ val zipBytes = createPluginZip(" hot-reload" , pluginXml)
287+
288+ val service = PluginHotReloadService ()
289+ val result = service.reloadPlugin(zipBytes)
290+
291+ assertFalse(" Should fail when trying to reload self" , result.success)
292+ assertTrue(" Should mention cannot reload itself" , result.message.contains(" cannot" , ignoreCase = true ) || result.message.contains(" Cannot" , ignoreCase = true ))
293+ assertEquals(" Plugin ID should be set" , getSelfPluginId(), result.pluginId)
294+ }
295+
296+ /* *
297+ * Tests that attempting to reload via jar structure is also blocked.
298+ */
299+ fun testSelfReloadFromJarIsBlocked () {
300+ val pluginXml = """
301+ <idea-plugin>
302+ <id>com.jonnyzzz.intellij.hot-reload</id>
303+ <name>Hot Reload Plugin</name>
304+ </idea-plugin>
305+ """ .trimIndent()
306+
307+ // Create the plugin jar with META-INF/plugin.xml
308+ val pluginJarBytes = ByteArrayOutputStream ().use { jarBaos ->
309+ ZipOutputStream (jarBaos).use { jarZos ->
310+ jarZos.putNextEntry(ZipEntry (" META-INF/" ))
311+ jarZos.closeEntry()
312+ jarZos.putNextEntry(ZipEntry (" META-INF/plugin.xml" ))
313+ jarZos.write(pluginXml.toByteArray(StandardCharsets .UTF_8 ))
314+ jarZos.closeEntry()
315+ }
316+ jarBaos.toByteArray()
317+ }
318+
319+ // Create the outer ZIP with nested jar structure
320+ val zipBytes = ByteArrayOutputStream ().use { baos ->
321+ ZipOutputStream (baos).use { zos ->
322+ zos.putNextEntry(ZipEntry (" intellij-plugin-hot-reload/" ))
323+ zos.closeEntry()
324+ zos.putNextEntry(ZipEntry (" intellij-plugin-hot-reload/lib/" ))
325+ zos.closeEntry()
326+ zos.putNextEntry(ZipEntry (" intellij-plugin-hot-reload/lib/intellij-plugin-hot-reload-1.0.0.jar" ))
327+ zos.write(pluginJarBytes)
328+ zos.closeEntry()
329+ }
330+ baos.toByteArray()
331+ }
332+
333+ val service = PluginHotReloadService ()
334+ val result = service.reloadPlugin(zipBytes)
335+
336+ assertFalse(" Should fail when trying to reload self from jar structure" , result.success)
337+ assertEquals(" Plugin ID should be set" , getSelfPluginId(), result.pluginId)
338+ }
339+
120340 fun testReloadPluginWithEmptyBytes () {
121341 val service = PluginHotReloadService ()
122342 val result = service.reloadPlugin(ByteArray (0 ))
0 commit comments